diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 84f4196..900c3d8 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -9,118 +9,15 @@ on: jobs: build-and-test: runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - - - name: Create Build Directory - run: mkdir build - - - name: Configure CMake - working-directory: ./build - run: cmake -DDISABLE_AVX512=ON -DENABLE_ADDRESS_SANITIZER=ON -DPIXIE_BENCHMARKS=OFF .. - - - name: Build Project - working-directory: ./build - run: make -j - - - name: Run Unittests - working-directory: ./build - run: ./unittests - - - name: Run LOUDS Tree Tests - working-directory: ./build - run: ./louds_tree_tests - - - name: Run BP Tree Tests - working-directory: ./build - run: ./bp_tree_tests - - - name: Run DFUDS Tree Tests - working-directory: ./build - run: ./dfuds_tree_tests - - - name: Run Benchmark Tests - working-directory: ./build - run: ./benchmark_tests - - - name: Run RmM Tree Tests - working-directory: ./build - run: ./test_rmm - - build-and-test-with-SDE: - runs-on: ubuntu-latest - timeout-minutes: 60 steps: - uses: actions/checkout@v4 - - name: Create Build Directory - run: mkdir build - - - name: Download and Unpack SDE - working-directory: ./build - run: | - wget https://downloadmirror.intel.com/859732/sde-external-9.58.0-2025-06-16-lin.tar.xz - tar xf sde-external-9.58.0-2025-06-16-lin.tar.xz - - name: Configure CMake - working-directory: ./build - run: cmake -DENABLE_ADDRESS_SANITIZER=ON -DMARCH=icelake-client -DHAVE_STD_REGEX=ON .. + run: cmake --preset asan - name: Build Project - working-directory: ./build - run: make -j - - - name: Run Unittests - working-directory: ./build - # SDE can hang during process teardown (static/ASan destructors) after all - # tests complete successfully. Use `timeout` to prevent the job from blocking - # indefinitely. GTest XML output is used to verify tests passed before - # treating a teardown timeout as success. - run: | - timeout 1800 sde-external-9.58.0-2025-06-16-lin/sde64 -icl -emu-xinuse 0 -- \ - ./unittests --gtest_output=xml:unittests_results.xml - rc=$? - if [ $rc -eq 124 ] && grep -q 'failures="0"' unittests_results.xml 2>/dev/null; then - echo "SDE timed out during process teardown (known SDE/ASan issue) - all tests passed, treating as success" - exit 0 - fi - exit $rc - - - name: Run LOUDS Tree Tests - working-directory: ./build - run: | - timeout 1800 sde-external-9.58.0-2025-06-16-lin/sde64 -icl -emu-xinuse 0 -- \ - ./louds_tree_tests --gtest_output=xml:louds_results.xml - rc=$? - if [ $rc -eq 124 ] && grep -q 'failures="0"' louds_results.xml 2>/dev/null; then - echo "SDE timed out during process teardown (known SDE/ASan issue) - all tests passed, treating as success" - exit 0 - fi - exit $rc - - - name: Run BP Tree Tests - working-directory: ./build - run: | - timeout 1800 sde-external-9.58.0-2025-06-16-lin/sde64 -icl -emu-xinuse 0 -- \ - ./bp_tree_tests --gtest_output=xml:bp_results.xml - rc=$? - if [ $rc -eq 124 ] && grep -q 'failures="0"' bp_results.xml 2>/dev/null; then - echo "SDE timed out during process teardown (known SDE/ASan issue) - all tests passed, treating as success" - exit 0 - fi - exit $rc - - - name: Run DFUDS Tree Tests - working-directory: ./build - run: | - timeout 1800 sde-external-9.58.0-2025-06-16-lin/sde64 -icl -emu-xinuse 0 -- \ - ./dfuds_tree_tests --gtest_output=xml:dfuds_results.xml - rc=$? - if [ $rc -eq 124 ] && grep -q 'failures="0"' dfuds_results.xml 2>/dev/null; then - echo "SDE timed out during process teardown (known SDE/ASan issue) - all tests passed, treating as success" - exit 0 - fi - exit $rc + run: cmake --build --preset asan --parallel + - name: Run tests + run: ctest --preset asan diff --git a/.github/workflows/doxygen.yml b/.github/workflows/doxygen.yml index 78bd82e..940eb64 100644 --- a/.github/workflows/doxygen.yml +++ b/.github/workflows/doxygen.yml @@ -39,14 +39,14 @@ jobs: sudo mv doxygen-1.13.2/bin/doxygen /usr/local/bin/doxygen shell: bash - name: Cmake configure - run: cmake -S ${{github.workspace}} -B ${{github.workspace}}/build -DPIXIE_DOCS=ON -DPIXIE_TESTS=OFF -DPIXIE_BENCHMARKS=OFF + run: cmake --preset docs - name: Build docs - run: cmake --build ${{github.workspace}}/build --target docs + run: cmake --build --preset docs - name: Upload artifact uses: actions/upload-pages-artifact@v3 with: # Upload entire repository - path: ${{github.workspace}}/build/docs/html + path: ${{github.workspace}}/build/docs/docs/html - name: Deploy to GitHub Pages id: deployment uses: actions/deploy-pages@v4 diff --git a/AGENTS.md b/AGENTS.md index 6c9acef..96b8f23 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,146 +2,261 @@ ## Project Overview -Pixie is a **succinct data structures library** written in C++20. It provides space-efficient data structures that use close to the theoretical minimum space while supporting efficient queries. The library targets practical performance for data sizes up to 2^64 bits. +Pixie is a **header-only succinct data structures library** written in C++20. +It targets practical performance for data sizes up to `2^64` bits while keeping +space usage close to the theoretical minimum. -Current implementations include BitVector, RmM Tree, and LOUDS Tree. Planned additions include wavelet trees, FM-index, compressed suffix arrays, and other succinct data structures. +Current library families are: -## Skills +- rank/select support over packed bit sequences; +- range min-max (RmM) indexes; +- static range-minimum-query (RMQ) indexes; +- rooted-tree encodings (LOUDS, balanced parentheses, and DFUDS); +- aligned owning storage, read-only storage views, and mapped-file resources; +- storage-backed wavelet-tree indexes. -Shared C++ agent skills live in `agentic/cpp/skills`. Pixie-specific examples -for those skills live in `agentic/local/cpp/skills`. -Shared C++ agent commands live in `agentic/cpp/commands`. Pixie-specific -commands or command notes live in `agentic/local/cpp/commands`. +Planned additions include FM-indexes, compressed suffix arrays, and other +succinct data structures. -When a task matches a skill, read: +## Shared Agentic Guidance + +`agentic/cpp` is the canonical source of reusable C++ agent guidance, skills, +and commands. Read `agentic/cpp/AGENTS.md` for shared workflow and skill policy +before relying on a shared skill or command. Do not duplicate that procedural +guidance in Pixie-specific instructions. + +Pixie-specific examples and additions live in `agentic/local/cpp`: + +- `agentic/local/cpp/skills//EXAMPLES.md` supplies local examples for a + matching shared skill. +- `agentic/local/cpp/commands/` supplies Pixie-specific command notes. + +When a task matches a skill, read the shared skill first and then its local +example, if present: 1. `agentic/cpp/skills//SKILL.md` -2. `agentic/local/cpp/skills//EXAMPLES.md`, if present +2. `agentic/local/cpp/skills//EXAMPLES.md` + +Use the shared skill or command as the canonical workflow. The local overlay +adds Pixie context; it does not replace the shared guidance. ## Architecture ### Project Layout Conventions -- **`include/`**: Header-only library API (all implementations here, no `.cpp` files) -- **`include/reference_implementations/`**: Naive reference implementations for differential testing -- **`src/*_tests.cpp`**: Unit tests (Google Test) -- **`src/*_benchmarks.cpp`**: Performance benchmarks (Google Benchmark) -- **`src/docs/`**: Doxygen configuration -- **`academic/`**: Academic materials — papers, presentations, reports, bibliography, notes +- **`include/pixie/`**: Header-only public API and implementations. +- **`include/pixie/.h`**: Lightweight CRTP contract for one library + family, such as `rank_select.h`, `rmq.h`, or `storage.h`. +- **`include/pixie//`**: Concrete implementations for that family. +- **`include/pixie//implementations.h`**: Catalog and umbrella include + for the family's concrete implementations. It is the single family-level + include for tests and benchmarks; a concrete implementation must not include + its own catalog. +- **`include/pixie/experimental/`**: Isolated experimental primitives and + implementations. Do not promote an experiment without tests and a registered + benchmark where performance is relevant. +- **`include/pixie/io/`**: Resource-owning I/O helpers, such as `MappedFile`. +- **`include/references/`**: Naive reference implementations used for + differential testing. +- **`src/tests/`**: Google Test suites. Family tests are consolidated into + shared specification suites rather than one executable per implementation. +- **`src/benchmarks/`**: Google Benchmark harnesses and common benchmark + helpers. +- **`src/docs/`**: Doxygen configuration and generated-documentation inputs. +- **`academic/`**: Durable scholarly and technical documentation: papers, + presentations, reports, research and lecture notes, shared figures, and the + canonical BibTeX bibliography. Follow `academic/AGENTS.md` within that tree. + +### Family Interface Pattern + +Public data-structure families use CRTP contracts. The current contracts are +`RankSelectBase`, `RmMBase`, `pixie::rmq::RmqBase`, `TreeBase`, `StorageBase`, +and `WaveletTreeBase`. + +1. Define or extend the public contract in `include/pixie/.h`. + Public facade methods delegate to a clearly named `*_impl()` method on the + concrete type. +2. Make each concrete implementation inherit its family contract and implement + the required `*_impl()` methods. Do not add virtual dispatch for this API. +3. Add the concrete header to the corresponding `implementations.h` catalog. + The catalog includes the contract and concrete headers, not the reverse. +4. Include the catalog in the family test and benchmark harness, then put every + compatible implementation through the same typed specification suite. + +The contract is the source of truth for observable semantics. Every public +facade operation and every extension-point requirement needs Doxygen +documentation that states its range convention, rank base, ties, sentinel +value, ownership/lifetime constraint, and invalid-input behavior where +applicable. There are no virtual public interfaces: document the CRTP facade +and its `*_impl()` contract instead of duplicating undocumented behavior in +each implementation. ### Key Design Decisions -1. **Header-only library** (see rationale below) -2. **Non-owning spans**: Data structures use `std::span` for external data where appropriate. -3. **SIMD conditional compilation**: Uses `#ifdef PIXIE_AVX512_SUPPORT` / `PIXIE_AVX2_SUPPORT` with scalar fallbacks. -4. **Target domain**: Optimized for data sizes up to 2^64 bits. -5. **Platform**: Linux/Unix is the primary target platform. -6. **Interval convention**: Public APIs, tests, and benchmarks use C++-style - half-open ranges `[left, right)`: `left` is included and `right` is excluded. - Empty ranges (`left == right`) are invalid unless an API explicitly documents - otherwise. Low-level primitives may still use their own documented interval - conventions. +1. **Header-only library**: implementations live under `include/`; there are + no Pixie `.cpp` library sources. +2. **Non-owning indexes**: indexes use external immutable data where + appropriate, commonly `std::span`. The caller keeps + that data alive and stable for the index lifetime. +3. **SIMD conditional compilation**: use `PIXIE_AVX512_SUPPORT` and + `PIXIE_AVX2_SUPPORT` guards with a scalar fallback. +4. **Target domain**: optimize for bit sequences and indexes up to `2^64` + bits. +5. **Platform**: Linux/Unix is the current target platform. `MappedFile` + requires POSIX `mmap`; there is no Windows implementation yet. +6. **Range conventions**: rank/select, RMQ, wavelet-tree, public tests, and + benchmarks normally use zero-based half-open ranges `[left, right)`. Do not + generalize this to every low-level primitive: RmM relative-excess range + queries explicitly use their documented inclusive bit range. State the + convention at every public boundary. +7. **Storage ownership**: keep mutable owners, read-only non-owning views, and + OS-backed resource owners as separate types. Storage subranges are + byte-oriented. A view never extends its backing owner's lifetime and can be + invalidated by owner resize or destruction. +8. **Optional adapters**: third-party implementations stay behind their build + option and must not become a library runtime dependency. ### Why Header-Only? -The library is header-only by design. This decision is based on analysis of similar libraries (notably sdsl-lite, which migrated from compiled to header-only in v3). - -**Advantages for Pixie:** +Pixie is header-only by design. Similar libraries, including sdsl-lite, have +made the same trade-off. | Benefit | Explanation | -|---------|-------------| -| **SIMD flexibility** | Users compile with their target `-march` flags, enabling optimal SIMD code paths | -| **Better inlining** | Compiler sees full implementation, enabling aggressive optimization for small hot functions (rank, select) | -| **No ABI issues** | Works across different compilers and standard library versions | -| **Easy integration** | Users just `#include` headers; no library linking or installation required | -| **Template-friendly** | Templates work naturally without explicit instantiation | +| --- | --- | +| SIMD flexibility | Users compile with their target `-march` flags and enable the best available code path. | +| Better inlining | The compiler sees hot rank/select and navigation code. | +| No ABI issues | The library works across compiler and standard-library versions. | +| Easy integration | Consumers include headers and do not link a Pixie binary. | +| Template-friendly | Templates need no explicit instantiations. | -**When to reconsider:** - -A compiled component may be warranted if Pixie adds: -- Heavy construction algorithms (e.g., suffix array construction) -- Large static lookup tables that shouldn't be duplicated -- Global runtime state (memory tracking, configuration) -- External C library dependencies +Reconsider a compiled component only for substantial construction algorithms, +large static tables that should not be duplicated, global runtime state, or an +unavoidable external C library dependency. ## Technology Stack -- **Language**: C++20 (required features: `std::span`, `std::popcount`, ``) -- **Build**: CMake >= 3.18 -- **Testing**: Google Test v1.17.0 -- **Benchmarking**: Google Benchmark v1.9.4 -- **SIMD**: AVX-512 (primary), AVX2 (fallback), scalar fallbacks -- **Style**: Chromium C++ style (`.clang-format`) +- **Language**: C++20 (`std::span`, `std::popcount`, and `` are required). +- **Build**: CMake 3.18 or newer. +- **Testing**: Google Test 1.17.0, registered with CTest. +- **Benchmarking**: Google Benchmark 1.9.4. +- **SIMD**: AVX-512 when available, AVX2 fallback, and scalar fallback. +- **Style**: Chromium C++ style (`.clang-format`). ### Dependencies -Pixie itself is header-only and has **no runtime dependencies**. Build-time dependencies are managed via CMake FetchContent and controlled by two options: - -| Option | Default | What it enables | -|--------|---------|-----------------| -| `PIXIE_TESTS` | `ON` | Unit tests (fetches Google Test) | -| `PIXIE_BENCHMARKS` | `ON` | Benchmarks + comparison benchmarks (fetches Google Benchmark, pasta-toolbox, sdsl-lite v3) | - -Third-party libraries (pasta-toolbox, sdsl-lite) are used **only** for comparison benchmarks, not by the library itself. - -## Build Commands +Pixie has no runtime or default third-party dependency. CMake fetches build-time +dependencies only as needed. Direct CMake defaults and their effects are: + +| Option | Default | Effect | +| --- | --- | --- | +| `PIXIE_TESTS` | `ON` standalone, `OFF` downstream | Builds tests and fetches Google Test. | +| `PIXIE_BENCHMARKS` | `OFF` | Builds native Google Benchmark targets. | +| `PIXIE_THIRD_PARTY_BACKENDS` | `OFF` | Enables optional SDSL adapters and their comparison targets. | +| `PIXIE_DIAGNOSTICS` | `OFF` | Enables diagnostic logging for profiling experiments. | +| `PIXIE_DOCS` | `OFF` | Enables the Doxygen `docs` target. | +| `PIXIE_COVERAGE` | `OFF` | Adds GCC coverage instrumentation. | + +`MappedFile` uses native POSIX memory mapping on Linux/Unix. A default +FetchContent consumer therefore receives no third-party dependency, while a +standalone default build fetches Google Test. Enabling third-party backends also +fetches SDSL and pasta-toolbox dependencies, but only SDSL currently has a +registered Pixie adapter/comparison benchmark. Do not describe pasta-toolbox as +an available backend until Pixie adds and registers one. + +## Build and Test Presets + +Use CMake presets for supported builds. They keep build directories isolated +under `build/` and export `compile_commands.json`. + +| Preset | Purpose | +| --- | --- | +| `debug` | Debug tests. | +| `release` | Optimized tests. | +| `asan` | Debug tests with AddressSanitizer and AVX-512 disabled. | +| `coverage` | Debug tests with GCC coverage instrumentation. | +| `benchmarks` | Native Pixie benchmarks. | +| `benchmark-all-backends` | Native benchmarks plus every currently registered optional backend target. | +| `benchmarks-profile` | RelWithDebInfo benchmarks with diagnostics and libpfm support. | +| `docs` | Doxygen documentation only. | ```bash -# Standard build (Release) -cmake -B build/release -DCMAKE_BUILD_TYPE=Release -cmake --build build/release -j - -# Debug build -cmake -B build/debug -DCMAKE_BUILD_TYPE=Debug -cmake --build build/debug -j - -# Without AVX-512 (AVX2 fallback) -cmake -B build/release -DDISABLE_AVX512=ON -cmake --build build/release -j - -# With AddressSanitizer -cmake -B build/asan -DENABLE_ADDRESS_SANITIZER=ON -cmake --build build/asan -j - -# Custom march flag -cmake -B build/release -DMARCH=icelake-client -cmake --build build/release -j +# Standard optimized test build. +cmake --preset release +cmake --build --preset release +ctest --preset release + +# Native benchmark build. +cmake --preset benchmarks +cmake --build --preset benchmarks + +# Comparison benchmarks, including the optional SDSL targets. +cmake --preset benchmark-all-backends +cmake --build --preset benchmark-all-backends + +# Documentation. +cmake --preset docs +cmake --build --preset docs +``` -# Tests only (no benchmarks or third-party deps) -cmake -B build/release -DPIXIE_BENCHMARKS=OFF -cmake --build build/release -j +For an ad hoc configuration, use a distinct build directory and pass explicit +options. Do not overwrite a supported preset build merely to test an unrelated +configuration. +```bash +cmake -S . -B build/local-avx2 -DCMAKE_BUILD_TYPE=Release \ + -DPIXIE_TESTS=ON -DDISABLE_AVX512=ON +cmake --build build/local-avx2 -j +ctest --test-dir build/local-avx2 --output-on-failure ``` ## Testing -### Running Tests +CTest is the canonical test entry point. Google Test discovery uses +`DISCOVERY_MODE PRE_TEST`, so CTest discovers and runs each test binary. ```bash -./build/release/unittests # BitVector tests -./build/release/test_rmm # RmM Tree tests -./build/release/rmq_tests # RMQ tests -./build/release/louds_tree_tests # LOUDS Tree tests +ctest --preset debug +ctest --preset release +ctest --preset asan +ctest --preset coverage + +# Run only a registered test-target label. +ctest --preset release -L rank_select_tests ``` +The registered test executables are `bit_algorithms_unittests`, +`rank_select_unittests`, `rank_select_tests`, `benchmark_tests`, `test_rmm`, +`tree_tests`, `wavelet_tree_tests`, `storage_tests`, +`excess_positions_tests`, `excess_record_lows_tests`, and `rmq_tests`. Run an +executable directly only when debugging a focused Google Test filter. + ### Test Configuration via Environment Variables -For `test_rmm.cpp`: -- `RMM_CASES`: Number of test cases -- `RMM_OPS`: Number of operations per case -- `RMM_MAX_N`: Maximum input size -- `RMM_SEED`: Random seed for reproducibility +- `EXCESS_POS_CASES`: randomized cases in `excess_positions_tests` (default + `1000`). +- `EXCESS_POS_SEED`: random seed in `excess_positions_tests` (default `42`). +- `RECORD_LOWS_CASES`: randomized cases in `excess_record_lows_tests` (default + `10000`). +- `RECORD_LOWS_SEED`: random seed in `excess_record_lows_tests` (default `42`). ### Testing Patterns -- **Differential testing**: Compare against naive reference implementations in `include/reference_implementations/` -- **Randomized testing**: Random bit vectors and balanced parentheses sequences -- **Exhaustive short inputs**: Test all patterns for small sizes -- **Shape-forcing tests**: For RMQ/RmM trees, deterministic tests that force - a specific traversal path are often more valuable than adding more random - cases. Target border correction, same-leaf paths, prefix/suffix leaf - selectors, top sparse-table hit/miss paths, partial last blocks, and - first-min tie cases. +- **Shared specifications**: add every conforming concrete implementation to + the family `TYPED_TEST` suite. A new implementation is not complete until it + passes the same public contract as its peers. +- **Differential testing**: compare behavior with an appropriate naive type in + `include/references/`. +- **Randomized testing**: use deterministic seeds and expose a seed only when + it helps reproduce a failure. +- **Exhaustive short inputs**: enumerate small bit patterns and tree shapes + when feasible. +- **Shape-forcing tests**: for RMQ and RmM, favor deterministic traversal-path + cases over more undirected random input. Cover border correction, same-leaf + paths, prefix/suffix selectors, sparse-overlay hit/miss paths, partial final + blocks, and first-minimum ties. +- **Storage tests**: test owners and views through the same specification, + including nested byte subranges, alignment constraints, serialization, and + owner/view lifetime rules. ### Coverage and Codecov @@ -151,85 +266,116 @@ Use the repository script for local coverage: ./scripts/coverage_report.sh ``` -This script configures the `coverage` preset, builds coverage targets, deletes -old `.gcda`/`.gcov` files, runs the test binaries, and writes +The script configures and builds the `coverage` preset, deletes stale +`.gcda`/`.gcov` files, runs `ctest --preset coverage`, and writes `build/coverage/coverage.txt`. -Coverage interpretation notes: - -- The report is generated with `gcov -pb`, so Codecov receives branch - probabilities in addition to line hits. Codecov "partial" lines are lines - that executed but did not cover all branch paths, for example short-circuit - conditions, ternaries, loop exits, or exception edges. -- Do not compare Codecov's branch-aware percentage directly with a plain - line-only local summary. Codecov effectively penalizes partially covered - branch lines. -- After recompiling an instrumented target, stale `.gcda` files can produce - `overwriting an existing profile data with a different checksum`. Run - `./scripts/coverage_report.sh` or delete the coverage `.gcda` files before - trusting a local coverage run. -- Header-only templates can appear in multiple gcov translation-unit reports. - For headers such as `experimental/rmm_btree.h`, inspect the generated - `.gcov` file or Codecov aggregate rather than trusting the first `File ...` - block in `coverage.txt`. -- For RMQ/RmM coverage work, prioritize public behavior paths over artificial - internal coverage. Useful tests cover HybridBTree leaf selector variants, - internal border correction, top sparse-overlay hit/miss behavior, - CartesianHybridBTree BP-depth query paths, and RmMBTree forward/backward - search/minselect edge cases. Low-value gaps include allocator failure paths, - `length_error` paths that require impossible index sizes, and private - defensive `npos` branches that cannot be reached through a valid public API. - -## Code Style Guidelines - -1. **Formatting**: Run `clang-format` before committing (Chromium style) -2. **Namespace**: All library code in `pixie` namespace -3. **Documentation**: Use Doxygen-style comments for public API -4. **Constants**: Use `constexpr` for compile-time values -5. **Alignment**: Be ware of data alignment, in most cases it is preferable to use 64-byte aligned array allocations +- The report uses `gcov -pb`, so Codecov receives branch probabilities as well + as line hits. A Codecov partial line executed but did not take every branch. +- Do not compare Codecov's branch-aware percentage with a line-only local + summary. +- After recompiling instrumented code, stale `.gcda` files can cause an + `overwriting an existing profile data with a different checksum` warning. + Run the script before trusting the report. +- Header-only templates can appear in multiple gcov translation units. For a + header such as `rmm/btree.h`, inspect its `.gcov` output or the Codecov + aggregate instead of trusting the first block in `coverage.txt`. +- Prioritize public behavior paths over unreachable defensive or allocator + failure paths. + +## Code Style and Documentation + +1. Run `clang-format` before committing; use the repository's Chromium style. +2. Put library code in the `pixie` namespace; RMQ contracts and implementations + use `pixie::rmq`. +3. Use Doxygen comments for every public API. Base/contract headers must fully + document each public facade operation and its implementation requirement. +4. Use `constexpr` for compile-time values. +5. Be aware of alignment. Prefer the 64-byte-aligned storage facilities where + a hot data structure benefits from cache-line alignment rather than adding + ad hoc aligned allocation code. +6. Keep public contracts lightweight. Do not include a family catalog from a + concrete header, and do not add compatibility forwarding headers unless the + user explicitly requests a compatibility layer. ## CI/CD Workflows -- **build-test.yml**: Builds with/without AVX-512, runs tests with ASan -- **linter.yml**: Clang-format checks on all C/C++ files -- **benchmarks-test.yml**: Performance benchmark runs -- **doxygen.yml**: Documentation generation +- **`build-test.yml`**: runs the AddressSanitizer fallback configuration + through its presets. +- **`linter.yml`**: checks formatting for C/C++ sources. +- **`benchmarks-test.yml`**: runs benchmark checks. +- **`doxygen.yml`**: configures and builds the `docs` preset, then publishes + `build/docs/docs/html`. ## Common Tasks for AI Agents -### Adding a New Data Structure - -1. Create header in `pixie/include/` with Doxygen documentation -2. Add unit tests in `src/tests/_tests.cpp` -3. Add benchmarks in `src/benchmarks/_benchmarks.cpp` -4. Update `CMakeLists.txt` with new executables -5. Run `clang-format` on new files +### Adding a Concrete Implementation + +1. Select the existing family contract. Add an operation to that contract only + when every implementation should expose it; document the semantics there. +2. Add the concrete header under `include/pixie//`, inherit the CRTP + base, and implement the required `*_impl()` methods. +3. Add it to `/implementations.h`; guard optional third-party adapters + with the established build macro. +4. Add it to the existing shared family specification suite and compare it with + a reference implementation where possible. Create a separate test target + only for a genuinely new family. +5. Add comparable rows to the established family benchmark harness. Create a + new benchmark target only for a genuinely new family. +6. Update `CMakeLists.txt` registration only when adding a new test or + benchmark target, then run formatting and the relevant preset tests. ### Modifying SIMD Code -1. Provide implementations for: - - AVX-512 (`#ifdef PIXIE_AVX512_SUPPORT`) - - AVX2 (`#ifdef PIXIE_AVX2_SUPPORT`) - - Scalar fallback -2. Test with `-DDISABLE_AVX512=ON` to verify fallback works -3. Benchmark to ensure performance is maintained +1. Keep an AVX-512 implementation, AVX2 implementation where useful, and a + scalar fallback behind the existing feature guards. +2. Include `` only in translation units or headers that use SIMD + intrinsics; do not make it a generic benchmark dependency. +3. Validate the fallback with the `asan` preset or an isolated + `DISABLE_AVX512=ON` build. +4. Build the relevant benchmark preset before claiming a performance result. + Use `benchmarks-profile` for hardware counters when supported by the host. ### Adding Tests -1. Use Google Test framework -2. Include naive reference implementation for differential testing -3. Add edge cases: empty input, single element, boundary conditions -4. Use random testing with configurable seed for reproducibility +1. Test the public CRTP facade, not private helper implementation details. +2. Add a new implementation to the shared typed specification suite. +3. Add reference, boundary, random, and shape-forcing cases proportional to + the changed behavior. +4. Use CTest preset runs for final validation. ## Performance Philosophy -- **Target domain**: Data sizes up to 2^64 bits with corresponding queries -- **Goal**: Best practical performance across the target domain (not just asymptotic complexity) -- **Approach**: Benchmark-driven optimization using Google Benchmark -- **SIMD**: Leverage vectorized operations where beneficial -- **Cache efficiency**: Align data structures to cache line boundaries (64 bytes) - -## References - -- [SPIDER Paper](https://github.com/williams-cs/spider) - Reference for BitVector implementation -- [pasta-toolbox](https://github.com/pasta-toolbox/bit_vector) - Reference implementation for comparison +- **Target domain**: data sizes up to `2^64` bits with corresponding queries. +- **Goal**: best practical performance across the target domain, not only + asymptotic complexity. +- **Approach**: benchmark-driven optimization using Google Benchmark. +- **SIMD**: use vectorized operations only when benchmarked behavior justifies + their complexity. +- **Cache efficiency**: respect 64-byte cache-line alignment for hot storage. + +### Benchmark Result Retention + +- Do not commit raw benchmark result files or ad hoc result tables for stable + implementations. +- A user-requested current snapshot may live in the relevant implementation + catalog header. Keep it rounded and reproducible, and visually align every + Markdown table column in source, including headers, separators, and numeric + values. +- Do not retain local JSON, failed probes, or before/after experiment history. +- Persist results for an experimental implementation only when that + implementation is in the experimental area and has a registered benchmark. + +## Academic Materials and References + +`academic/` is Pixie's durable research and technical documentation area. Put +new papers, presentations, reports, research or lecture notes, figures, and +scholarly references there rather than scattering them across implementation +guidance or temporary root-level notes. Its canonical bibliography is +`academic/bibliography/references.bib`. + +Academic material currently renders as independent Quarto documents. It is a +potential first-class part of Pixie's public documentation, but is not an input +to the CMake/Doxygen `docs` target today. Do not couple an academic document to +that pipeline incidentally; propose a dedicated publication and navigation +change when integrating it into the public documentation site. diff --git a/CMakeLists.txt b/CMakeLists.txt index 6785c19..6101a98 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,11 @@ cmake_minimum_required(VERSION 3.18) project(pixie) +set(PIXIE_IS_TOP_LEVEL OFF) +if (CMAKE_CURRENT_SOURCE_DIR STREQUAL CMAKE_SOURCE_DIR) + set(PIXIE_IS_TOP_LEVEL ON) +endif () + set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) @@ -33,7 +38,7 @@ endif () # --------------------------------------------------------------------------- # Build options # --------------------------------------------------------------------------- -option(PIXIE_TESTS "Build unit tests" ON) +option(PIXIE_TESTS "Build unit tests" ${PIXIE_IS_TOP_LEVEL}) option(PIXIE_BENCHMARKS "Build benchmarks" OFF) option(PIXIE_THIRD_PARTY_BACKENDS "Build optional third-party backend integrations" OFF) option(PIXIE_DIAGNOSTICS "Include diagnostic logs" OFF) @@ -119,12 +124,23 @@ if (PIXIE_TESTS) include(GoogleTest) endif () -FetchContent_Declare( - mio - GIT_REPOSITORY https://github.com/vimpunk/mio.git - GIT_TAG master -) -FetchContent_MakeAvailable(mio) +# Downstream consumers link this target; Pixie itself remains header-only. +add_library(pixie INTERFACE) +add_library(pixie::pixie ALIAS pixie) +target_compile_features(pixie INTERFACE cxx_std_20) +target_include_directories(pixie INTERFACE + $) + +if (PIXIE_DIAGNOSTICS) + target_compile_definitions(pixie INTERFACE PIXIE_DIAGNOSTICS) + target_link_libraries(pixie INTERFACE spdlog::spdlog_header_only) +endif () + +if (PIXIE_THIRD_PARTY_BACKENDS) + target_compile_definitions(pixie INTERFACE SDSL_SUPPORT) + target_include_directories(pixie INTERFACE + $) +endif () # --------------------------------------------------------------------------- # Unit tests @@ -132,14 +148,31 @@ FetchContent_MakeAvailable(mio) if (PIXIE_TESTS) enable_testing() - add_executable(unittests - src/tests/unittests.cpp) - target_include_directories(unittests + add_executable(bit_algorithms_unittests + src/tests/bits_unittests.cc) + target_include_directories(bit_algorithms_unittests PUBLIC include) - target_link_libraries(unittests + target_link_libraries(bit_algorithms_unittests + gtest + gtest_main + ${PIXIE_DIAGNOSTICS_LIBS}) + + add_executable(rank_select_unittests + src/tests/rank_select_unittests.cc) + target_include_directories(rank_select_unittests + PUBLIC include) + target_link_libraries(rank_select_unittests + gtest + gtest_main + ${PIXIE_DIAGNOSTICS_LIBS}) + + add_executable(rank_select_tests + src/tests/rank_select_tests.cpp) + target_include_directories(rank_select_tests + PUBLIC include) + target_link_libraries(rank_select_tests gtest gtest_main - mio ${PIXIE_DIAGNOSTICS_LIBS}) add_executable(benchmark_tests @@ -149,7 +182,6 @@ if (PIXIE_TESTS) target_link_libraries(benchmark_tests gtest gtest_main - mio ${PIXIE_DIAGNOSTICS_LIBS}) add_executable(test_rmm @@ -160,21 +192,19 @@ if (PIXIE_TESTS) target_link_libraries(test_rmm gtest gtest_main - mio ${PIXIE_DIAGNOSTICS_LIBS}) if (PIXIE_THIRD_PARTY_BACKENDS) target_include_directories(test_rmm PRIVATE ${sdsl_lite_SOURCE_DIR}/include) endif () - add_executable(louds_tree_tests - src/tests/louds_tree_tests.cpp) - target_include_directories(louds_tree_tests + add_executable(tree_tests + src/tests/tree_tests.cpp) + target_include_directories(tree_tests PUBLIC include) - target_link_libraries(louds_tree_tests + target_link_libraries(tree_tests gtest gtest_main - mio ${PIXIE_DIAGNOSTICS_LIBS}) add_executable(wavelet_tree_tests @@ -184,37 +214,24 @@ if (PIXIE_TESTS) target_link_libraries(wavelet_tree_tests gtest gtest_main - mio ${PIXIE_DIAGNOSTICS_LIBS}) - add_executable(excess_positions_tests - src/tests/excess_positions_tests.cpp) - target_include_directories(excess_positions_tests + add_executable(storage_tests + src/tests/storage_tests.cpp) + target_include_directories(storage_tests PUBLIC include) - target_link_libraries(excess_positions_tests - gtest - gtest_main - mio - ${PIXIE_DIAGNOSTICS_LIBS}) - - add_executable(bp_tree_tests - src/tests/bp_tree_tests.cpp) - target_include_directories(bp_tree_tests - PUBLIC include) - target_link_libraries(bp_tree_tests + target_link_libraries(storage_tests gtest gtest_main - mio ${PIXIE_DIAGNOSTICS_LIBS}) - add_executable(dfuds_tree_tests - src/tests/dfuds_tree_tests.cpp) - target_include_directories(dfuds_tree_tests + add_executable(excess_positions_tests + src/tests/excess_positions_tests.cpp) + target_include_directories(excess_positions_tests PUBLIC include) - target_link_libraries(dfuds_tree_tests + target_link_libraries(excess_positions_tests gtest gtest_main - mio ${PIXIE_DIAGNOSTICS_LIBS}) add_executable(excess_record_lows_tests @@ -233,74 +250,89 @@ if (PIXIE_TESTS) target_link_libraries(rmq_tests gtest gtest_main - mio ${PIXIE_DIAGNOSTICS_LIBS}) if (PIXIE_THIRD_PARTY_BACKENDS) target_include_directories(rmq_tests PRIVATE ${sdsl_lite_SOURCE_DIR}/include) endif () + + set(PIXIE_TEST_TARGETS + bit_algorithms_unittests + rank_select_unittests + rank_select_tests + benchmark_tests + test_rmm + tree_tests + wavelet_tree_tests + storage_tests + excess_positions_tests + excess_record_lows_tests + rmq_tests) + foreach (test_target IN LISTS PIXIE_TEST_TARGETS) + gtest_discover_tests(${test_target} + DISCOVERY_MODE PRE_TEST + TEST_PREFIX "${test_target}." + PROPERTIES LABELS "${test_target}") + endforeach () endif () # --------------------------------------------------------------------------- # Benchmarks (Pixie-only and comparison benchmarks) # --------------------------------------------------------------------------- if (PIXIE_BENCHMARKS) - add_executable(benchmarks - src/benchmarks/benchmarks.cpp) - target_include_directories(benchmarks + add_executable(rank_select_benchmarks + src/benchmarks/rank_select_benchmarks.cpp) + target_include_directories(rank_select_benchmarks PUBLIC include) - target_link_libraries(benchmarks + target_link_libraries(rank_select_benchmarks benchmark - benchmark_main - mio ${PIXIE_DIAGNOSTICS_LIBS}) - if (PIXIE_THIRD_PARTY_BACKENDS) - target_link_libraries(benchmarks pasta_bit_vector) - target_compile_definitions(benchmarks PRIVATE PIXIE_THIRD_PARTY_BENCHMARKS) - else () - target_link_libraries(mio) - endif () - add_executable(bench_rmm - src/benchmarks/bench_rmm.cpp) - target_include_directories(bench_rmm + add_executable(rmm_benchmarks + src/benchmarks/rmm_benchmarks.cpp) + target_include_directories(rmm_benchmarks PUBLIC include) - target_link_libraries(bench_rmm + target_link_libraries(rmm_benchmarks benchmark - mio ${PIXIE_DIAGNOSTICS_LIBS}) + if (PIXIE_THIRD_PARTY_BACKENDS) + target_include_directories(rmm_benchmarks + PRIVATE ${sdsl_lite_SOURCE_DIR}/include) + endif () - add_executable(bench_rmm_btree - src/benchmarks/bench_rmm_btree.cpp) - target_include_directories(bench_rmm_btree + add_executable(rmm_btree_benchmarks + src/benchmarks/rmm_btree_benchmarks.cpp) + target_include_directories(rmm_btree_benchmarks PUBLIC include) - target_link_libraries(bench_rmm_btree + target_link_libraries(rmm_btree_benchmarks benchmark - mio ${PIXIE_DIAGNOSTICS_LIBS}) + if (PIXIE_THIRD_PARTY_BACKENDS) + target_include_directories(rmm_btree_benchmarks + PRIVATE ${sdsl_lite_SOURCE_DIR}/include) + endif () - add_executable(bench_rmq - src/benchmarks/bench_rmq.cpp) - target_include_directories(bench_rmq + add_executable(rmq_benchmarks + src/benchmarks/rmq_benchmarks.cpp) + target_include_directories(rmq_benchmarks PUBLIC include) - target_link_libraries(bench_rmq + target_link_libraries(rmq_benchmarks benchmark - mio ${PIXIE_DIAGNOSTICS_LIBS}) if (PIXIE_THIRD_PARTY_BACKENDS) - target_include_directories(bench_rmq + target_include_directories(rmq_benchmarks PRIVATE ${sdsl_lite_SOURCE_DIR}/include) - target_compile_definitions(bench_rmq + target_compile_definitions(rmq_benchmarks PRIVATE PIXIE_THIRD_PARTY_BENCHMARKS) endif () if (PIXIE_THIRD_PARTY_BACKENDS) - add_executable(bench_rmm_sdsl - src/benchmarks/bench_rmm_sdsl.cpp) - target_include_directories(bench_rmm_sdsl + add_executable(rmm_sdsl_benchmarks + src/benchmarks/rmm_sdsl_benchmarks.cpp) + target_include_directories(rmm_sdsl_benchmarks PUBLIC include PRIVATE ${sdsl_lite_SOURCE_DIR}/include) - target_link_libraries(bench_rmm_sdsl + target_link_libraries(rmm_sdsl_benchmarks PRIVATE benchmark ${PIXIE_DIAGNOSTICS_LIBS}) @@ -313,7 +345,6 @@ if (PIXIE_BENCHMARKS) target_link_libraries(louds_tree_benchmarks benchmark benchmark_main - mio ${PIXIE_DIAGNOSTICS_LIBS}) add_executable(wavelet_tree_benchmarks @@ -323,7 +354,6 @@ if (PIXIE_BENCHMARKS) target_link_libraries(wavelet_tree_benchmarks benchmark benchmark_main - mio ${PIXIE_DIAGNOSTICS_LIBS}) add_executable(bp_tree_benchmarks @@ -333,7 +363,6 @@ if (PIXIE_BENCHMARKS) target_link_libraries(bp_tree_benchmarks benchmark benchmark_main - mio ${PIXIE_DIAGNOSTICS_LIBS}) if (PIXIE_THIRD_PARTY_BACKENDS) target_include_directories(bp_tree_benchmarks @@ -347,21 +376,19 @@ if (PIXIE_BENCHMARKS) target_link_libraries(dfuds_tree_benchmarks benchmark benchmark_main - mio ${PIXIE_DIAGNOSTICS_LIBS}) if (PIXIE_THIRD_PARTY_BACKENDS) target_include_directories(dfuds_tree_benchmarks PRIVATE ${sdsl_lite_SOURCE_DIR}/include) endif () - add_executable(alignment_comparison - src/benchmarks/alignment_comparison.cpp) - target_include_directories(alignment_comparison + add_executable(alignment_comparison_benchmarks + src/benchmarks/alignment_comparison_benchmarks.cpp) + target_include_directories(alignment_comparison_benchmarks PUBLIC include) - target_link_libraries(alignment_comparison + target_link_libraries(alignment_comparison_benchmarks benchmark benchmark_main - mio ${PIXIE_DIAGNOSTICS_LIBS}) add_executable(excess_positions_benchmarks @@ -371,7 +398,6 @@ if (PIXIE_BENCHMARKS) target_link_libraries(excess_positions_benchmarks benchmark benchmark_main - mio ${PIXIE_DIAGNOSTICS_LIBS}) endif () diff --git a/CMakePresets.json b/CMakePresets.json index 0cb0cc6..b37cbb7 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -9,99 +9,101 @@ { "name": "base", "hidden": true, + "binaryDir": "${sourceDir}/build/${presetName}", "cacheVariables": { - "CMAKE_EXPORT_COMPILE_COMMANDS": "ON" - } - }, - { - "name": "debug", - "displayName": "Debug", - "inherits": "base", - "binaryDir": "${sourceDir}/build/debug", - "cacheVariables": { - "CMAKE_BUILD_TYPE": "Debug", + "CMAKE_EXPORT_COMPILE_COMMANDS": "ON", + "DISABLE_AVX512": "OFF", + "ENABLE_ADDRESS_SANITIZER": "OFF", "PIXIE_BENCHMARKS": "OFF", - "PIXIE_TESTS": "ON" + "PIXIE_COVERAGE": "OFF", + "PIXIE_DIAGNOSTICS": "OFF", + "PIXIE_DOCS": "OFF", + "PIXIE_TESTS": "OFF", + "PIXIE_THIRD_PARTY_BACKENDS": "OFF" } }, { - "name": "release", - "displayName": "Release", + "name": "test-base", + "hidden": true, "inherits": "base", - "binaryDir": "${sourceDir}/build/release", "cacheVariables": { - "CMAKE_BUILD_TYPE": "Release", - "PIXIE_BENCHMARKS": "OFF", "PIXIE_TESTS": "ON" } }, { - "name": "benchmarks-all", - "displayName": "Benchmarks", + "name": "benchmark-base", + "hidden": true, "inherits": "base", - "binaryDir": "${sourceDir}/build/release", "cacheVariables": { "CMAKE_BUILD_TYPE": "Release", - "PIXIE_TESTS": "OFF", - "PIXIE_BENCHMARKS": "ON", - "PIXIE_THIRD_PARTY_BACKENDS": "OFF" + "PIXIE_BENCHMARKS": "ON" } }, { - "name": "benchmarks-third-party", - "displayName": "Benchmarks with third-party backends", - "inherits": "base", - "binaryDir": "${sourceDir}/build/release-third-party", + "name": "debug", + "displayName": "Debug", + "inherits": "test-base", "cacheVariables": { - "CMAKE_BUILD_TYPE": "Release", - "PIXIE_BENCHMARKS": "ON", - "PIXIE_TESTS": "OFF", - "PIXIE_THIRD_PARTY_BACKENDS": "ON" + "CMAKE_BUILD_TYPE": "Debug" } }, { - "name": "benchmarks-diagnostic", - "displayName": "Benchmarks diagnostic build", - "inherits": "base", - "binaryDir": "${sourceDir}/build/release-with-deb", + "name": "release", + "displayName": "Release", + "inherits": "test-base", "cacheVariables": { - "BENCHMARK_ENABLE_LIBPFM": "ON", - "CMAKE_BUILD_TYPE": "RelWithDebInfo", - "PIXIE_DIAGNOSTICS": "ON", - "PIXIE_TESTS": "OFF", - "PIXIE_BENCHMARKS": "ON" + "CMAKE_BUILD_TYPE": "Release" } }, { - "name": "docs", - "displayName": "Docs", - "inherits": "base", - "binaryDir": "${sourceDir}/build/docs", + "name": "asan", + "displayName": "AddressSanitizer", + "inherits": "test-base", "cacheVariables": { - "CMAKE_BUILD_TYPE": "Release", - "PIXIE_DOCS": "ON" + "CMAKE_BUILD_TYPE": "Debug", + "DISABLE_AVX512": "ON", + "ENABLE_ADDRESS_SANITIZER": "ON" } }, { "name": "coverage", "displayName": "Coverage", - "inherits": "base", - "binaryDir": "${sourceDir}/build/coverage", + "inherits": "test-base", "cacheVariables": { "CMAKE_BUILD_TYPE": "Debug", - "PIXIE_BENCHMARKS": "OFF", "PIXIE_COVERAGE": "ON" } }, { - "name": "asan", - "displayName": "AddressSanitizer", + "name": "benchmarks", + "displayName": "Benchmarks", + "inherits": "benchmark-base" + }, + { + "name": "benchmark-all-backends", + "displayName": "Benchmarks with all backends", + "inherits": "benchmark-base", + "cacheVariables": { + "PIXIE_THIRD_PARTY_BACKENDS": "ON" + } + }, + { + "name": "benchmarks-profile", + "displayName": "Profile benchmarks", + "inherits": "benchmark-base", + "cacheVariables": { + "BENCHMARK_ENABLE_LIBPFM": "ON", + "CMAKE_BUILD_TYPE": "RelWithDebInfo", + "PIXIE_DIAGNOSTICS": "ON" + } + }, + { + "name": "docs", + "displayName": "Documentation", "inherits": "base", - "binaryDir": "${sourceDir}/build/asan", "cacheVariables": { - "CMAKE_BUILD_TYPE": "Debug", - "PIXIE_BENCHMARKS": "OFF", - "ENABLE_ADDRESS_SANITIZER": "ON" + "CMAKE_BUILD_TYPE": "Release", + "PIXIE_DOCS": "ON" } } ], @@ -109,127 +111,74 @@ { "name": "debug", "displayName": "Build Debug", - "configurePreset": "debug", - "targets": [ - "unittests", - "benchmark_tests", - "rmq_tests", - "test_rmm", - "louds_tree_tests", - "wavelet_tree_tests", - "bp_tree_tests", - "dfuds_tree_tests", - "excess_positions_tests", - "wavelet_tree_tests" - ] + "configurePreset": "debug" }, { "name": "release", "displayName": "Build Release", - "configurePreset": "release", - "targets": [ - "unittests", - "benchmark_tests", - "rmq_tests", - "test_rmm", - "louds_tree_tests", - "wavelet_tree_tests", - "bp_tree_tests", - "dfuds_tree_tests", - "excess_positions_tests", - "wavelet_tree_tests" - ] + "configurePreset": "release" + }, + { + "name": "asan", + "displayName": "Build AddressSanitizer", + "configurePreset": "asan" + }, + { + "name": "coverage", + "displayName": "Build Coverage", + "configurePreset": "coverage" }, { - "name": "benchmarks-all", + "name": "benchmarks", "displayName": "Build Benchmarks", - "configurePreset": "benchmarks-all", - "targets": [ - "benchmarks", - "bench_rmm", - "bench_rmq", - "louds_tree_benchmarks", - "bp_tree_benchmarks", - "dfuds_tree_benchmarks", - "alignment_comparison", - "excess_positions_benchmarks", - "wavelet_tree_benchmarks" - ] + "configurePreset": "benchmarks" }, { - "name": "benchmarks-third-party", - "displayName": "Build Benchmarks with third-party backends", - "configurePreset": "benchmarks-third-party", - "targets": [ - "benchmarks", - "bench_rmm", - "bench_rmm_sdsl", - "bench_rmq", - "louds_tree_benchmarks", - "bp_tree_benchmarks", - "dfuds_tree_benchmarks", - "alignment_comparison", - "excess_positions_benchmarks", - "wavelet_tree_benchmarks" - ] + "name": "benchmark-all-backends", + "displayName": "Build Benchmarks with all backends", + "configurePreset": "benchmark-all-backends" }, { - "name": "benchmarks-diagnostic", - "displayName": "Benchmarks diagnostic", - "configurePreset": "benchmarks-diagnostic", - "targets": [ - "benchmarks", - "bench_rmm", - "bench_rmq", - "louds_tree_benchmarks", - "bp_tree_benchmarks", - "dfuds_tree_benchmarks", - "alignment_comparison", - "excess_positions_benchmarks", - "wavelet_tree_benchmarks" - ] + "name": "benchmarks-profile", + "displayName": "Build Profile Benchmarks", + "configurePreset": "benchmarks-profile" }, { "name": "docs", - "displayName": "Build Docs", + "displayName": "Build Documentation", "configurePreset": "docs", "targets": [ "docs" ] + } + ], + "testPresets": [ + { + "name": "test-base", + "hidden": true, + "output": { + "outputOnFailure": true + } }, { - "name": "coverage", - "displayName": "Build Coverage", - "configurePreset": "coverage", - "targets": [ - "unittests", - "benchmark_tests", - "rmq_tests", - "test_rmm", - "louds_tree_tests", - "wavelet_tree_tests", - "bp_tree_tests", - "dfuds_tree_tests", - "excess_positions_tests", - "wavelet_tree_tests" - ] + "name": "debug", + "inherits": "test-base", + "configurePreset": "debug" + }, + { + "name": "release", + "inherits": "test-base", + "configurePreset": "release" }, { "name": "asan", - "displayName": "Build AddressSanitizer", - "configurePreset": "asan", - "targets": [ - "unittests", - "benchmark_tests", - "rmq_tests", - "test_rmm", - "louds_tree_tests", - "wavelet_tree_tests", - "bp_tree_tests", - "dfuds_tree_tests", - "excess_positions_tests", - "wavelet_tree_tests" - ] + "inherits": "test-base", + "configurePreset": "asan" + }, + { + "name": "coverage", + "inherits": "test-base", + "configurePreset": "coverage" } ] } diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a90ba65 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2026 Pixie contributors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md index 56ad227..a5182a2 100644 --- a/README.md +++ b/README.md @@ -1,348 +1,146 @@ # Pixie -Pixie logo +Pixie logo [![Build & Test](https://github.com/Malkovsky/pixie/actions/workflows/build-test.yml/badge.svg?branch=main)](https://github.com/Malkovsky/pixie/actions/workflows/build-test.yml) [![codecov](https://codecov.io/github/Malkovsky/pixie/graph/badge.svg?token=413VBX7M2U)](https://codecov.io/github/Malkovsky/pixie) [![Documentation](https://img.shields.io/badge/docs-doxygen-green.svg)](https://malkovsky.github.io/pixie/) -`pixie` is a **succinct data structures library**. +`pixie` is a **succinct data structures library**. A general concept behind this kind of structures is to be very compact yet still processible without total decompression. One might want this kind of structures in a case when storage space is limited and CPU performance is not tight as typically succinct structures are behind in performance compared to traditional data structures.
---- - -## Features - -* **BitVector** - * Data structure with 3.61% overhead supporting rank and select for 1 bits. - * Supports: - * `rank(i)`: number of set bits (`1`s) up to position `i`. - * `select(k)`: position of the `k`-th set bit. - * Similar operations `rank0/select0` for `0`. - * Implementation mainly follows [1] with SIMD optimizations similar to [2] - * Optimized via AVX-512/AVX-2, for large binary sequences performance is I/O bounded. -* **RmMTree** - * Implementation of a range min-max tree, it supports `rank`, `select` and `excess`-related operations allowing for a fast navigation in DFUDS/BP trees. - ---- - -## Requirements +## Outline -* C++20 -* [CMake](https://cmake.org/) ≥ 3.18. +- **Rank/select support**: fundamental primitive to efficiently map bits and their positions in a bit vector. +- **Range min-max tree**: fundamental primitive for efficient computation of excess queries mainly for navigation in balanced parenthesis sequences. +- **Succinct trees**: static variants of $2$-bit per entry trees, i.e. LOUDS, DFUDS, BP (Based on Euler tour and Ferrada-Navarro style). +- **Wavelet tree**, i.e. static structure that supposts rank/select on arbitrary finite alphabets, supports building a Huffman archieve with fast extraction of arbitrary segment. +- Succinct **cartesian tree** and a state of the art solution to static **RMQ** (array is immutable, queries are not known in advance). --- -## Build Instructions - -```sh -git clone https://github.com/Malkovsky/pixie.git -cd pixie -cmake --preset release -cmake --build --preset release -``` - -Manual alternative: +## Getting started -```sh -mkdir -p build/release -cmake -B build/release -DCMAKE_BUILD_TYPE=Release -cmake --build build/release -j -``` - -Tests are enabled by default (`PIXIE_TESTS=ON`). Benchmarks are opt-in; enable with `-DPIXIE_BENCHMARKS=ON` or configure with the `benchmarks-all` preset. Use `benchmarks-third-party` for comparison backends such as sdsl-lite, and `benchmarks-diagnostic` for performance diagnostics (Release with debug info + performance counters support). +Pixie requires C++20 and CMake 3.18 or newer. It is a header-only library and +is intended to be used via CMake FetchContent and the `pixie::pixie` target: ---- +```cmake +include(FetchContent) -## Running Tests +FetchContent_Declare( + pixie + GIT_REPOSITORY https://github.com/Malkovsky/pixie.git + GIT_TAG +) +FetchContent_MakeAvailable(pixie) -After building with presets, binaries are located in `build/release`. - -### BitVector - -```sh -./build/release/unittests +target_link_libraries(my_target PRIVATE pixie::pixie) ``` -### RmM Tree +Pixie exposes lightweight CRTP contracts and catalogs of concrete +implementations. For example, check the `pixie/rmq/implementations.h` to see all the available implementations for RMQ and include what you need. -```sh -./build/release/test_rmm -``` - ---- - -## Coverage - -Configure a coverage build with GCC (benchmarks disabled): +```cpp +#include +#include -```sh -cmake --preset coverage -cmake --build --preset coverage -``` +#include -Run tests and generate the gcov text report: +const std::array values = {7, 3, 5, 1, 4, 1}; +const pixie::rmq::HybridBTree rmq{std::span(values)}; -```sh -./scripts/coverage_report.sh +const auto position = rmq.arg_min(1, 6); // 3: first minimum in [1, 6) +const auto minimum = rmq.range_min(1, 6); // 1 ``` --- -## Running Benchmarks +## Tests and benchmarks -Before running benchmarks, configure with presets: +Pixie uses CTest for its internal suite: ```sh -cmake --preset benchmarks-all -cmake --build --preset release -``` - -For a RelWithDebInfo diagnostic build, use: - -```sh -cmake --preset benchmarks-diagnostic +cmake --preset release cmake --build --preset release +ctest --preset release ``` -### BitVector +To benchmark another implementation against Pixie, implement the corresponding +interface and register it in the family benchmark harness. -Benchmarks are random 50/50 0-1 bitvectors up to $2^{34}$ bits. +Here's an example using the optional SDSL RmM available for comparison through the +all-backends preset: ```sh -./build/release/benchmarks +cmake --preset benchmark-all-backends +cmake --build --preset benchmark-all-backends +./build/benchmark-all-backends/rmm_sdsl_benchmarks ``` -Write JSON and plot size-scaled benchmark curves with a log-scaled x-axis: - -```sh -./build/release/benchmarks --benchmark_out=bitvector_bench.json --benchmark_out_format=json -python3 scripts/plot_size_benchmarks.py bitvector_bench.json -o graphs/bitvector_size.png --size-key n -``` - -### Excess Positions - -```sh -./build/release/excess_positions_benchmarks --benchmark_out=excess_positions.json --benchmark_out_format=json -python3 scripts/excess_benchmark_table.py excess_positions.json -o src/docs/excess_positions_benchmark_results.md -``` - -Generated benchmark documentation can be written to `src/docs/benchmark_results.md`; -the documentation pipeline does not run benchmarks. - -### Adding an RMQ Benchmark - -Value RMQ implementations are benchmarked through the common CRTP interface in -`pixie::rmq::RmqBase`. To add a comparable backend, implement a non-owning index -that can be constructed from `std::span` and provides: - -* `size_impl()` -* `arg_min_impl(left, right)` for half-open ranges `[left, right)` -* `value_at_impl(position)` - -The public `size()`, `empty()`, `arg_min()`, and `range_min()` methods are then -provided by `RmqBase`. Ties should return the smaller original position. - -Minimal example: +`SdslRmMTree` adapts `sdsl::bp_support_sada<>` to the same CRTP facade as +Pixie's native RmM implementations. The following abridged excerpt shows the +connection; the full adapter is in `include/pixie/rmm/sdsl.h`. ```cpp -#include - -#include -#include -#include - -namespace pixie::rmq { - -template > -class LinearRmq : public RmqBase, T> { +#ifdef SDSL_SUPPORT +class SdslRmMTree : public RmMBase { public: - using Self = LinearRmq; - static constexpr std::size_t npos = RmqBase::npos; + using BpSupport = sdsl::bp_support_sada<>; + static constexpr std::size_t npos = RmMBase::npos; - explicit LinearRmq(std::span values, Compare compare = Compare()) - : values_(values), compare_(compare) {} + std::size_t size_impl() const { return size_; } - std::size_t size_impl() const { return values_.size(); } - - std::size_t arg_min_impl(std::size_t left, std::size_t right) const { - if (left >= right || right > values_.size()) { - return npos; - } - std::size_t best = left; - for (std::size_t i = left + 1; i < right; ++i) { - if (compare_(values_[i], values_[best])) { - best = i; - } + std::size_t rank1_impl(std::size_t end_position) const { + if (size_ == 0 || end_position == 0) { + return 0; } - return best; + return tree_.rank(std::min(end_position, size_) - 1); } - T value_at_impl(std::size_t position) const { return values_[position]; } + std::size_t close_impl(std::size_t open_position) const { + if (size_ == 0) { + return 0; + } + const std::size_t position = tree_.find_close(open_position); + return position < size_ ? position : npos; + } private: - std::span values_; - Compare compare_; + std::size_t size_{}; + sdsl::bit_vector bits_; + BpSupport tree_; }; - -} // namespace pixie::rmq -``` - -Then add rows to `src/benchmarks/bench_rmq.cpp` in `register_benchmarks()`. -Use `run_value_rmq_build` for construction cost and `run_queries` for query -cost: - -```cpp -benchmark::RegisterBenchmark( - "rmq_build_linear", - &run_value_rmq_build>>) - ->Arg(static_cast(size)) - ->Unit(benchmark::kMillisecond); - -benchmark::RegisterBenchmark( - "rmq_linear", - &run_queries>>) - ->Args({static_cast(size), - static_cast(width)}) - ->Unit(benchmark::kNanosecond); -``` - -The RMQ benchmark harness rotates through several value arrays so results are -less dependent on the global-minimum position. The `width` argument is the -maximum query width, not an exact width. - -To compare the new backend with `HybridBTree`, run both benchmark families -with a Google Benchmark filter. For example, after registering the new backend -as `rmq_linear` and `rmq_build_linear`: - -```sh -./build/release/bench_rmq \ - --benchmark_filter='^(rmq_linear|rmq_hybrid_btree)/(4194304|16777216)/(64|4096|262144|4194304|16777216)$' - -./build/release/bench_rmq \ - --benchmark_filter='^(rmq_build_linear|rmq_build_hybrid_btree)/(262144|4194304|16777216)$' -``` - -The first command compares query time for `2^22` and `2^24` input sizes across -the common RMQ widths. The second command compares construction time for the -same implementations. - -For hardware counters, use the diagnostic preset, which builds Google Benchmark -with libpfm support: - -```sh -cmake --preset benchmarks-diagnostic -cmake --build --preset benchmarks-diagnostic -j - -./build/release-with-deb/bench_rmq \ - --benchmark_filter='rmq_cartesian_hybrid_btree/67108864/4096' \ - --benchmark_perf_counters=CYCLES,INSTRUCTIONS,CACHE-MISSES \ - --benchmark_counters_tabular=true -``` - -Counter names are platform/libpfm dependent. Google Benchmark pauses timing and -perf counters during `state.PauseTiming()`, so RMQ dataset-variant rebuilds are -excluded from query counter rows. - -### RmM Tree - -```sh -./build/release/bench_rmm -``` - -For focused runs, `bench_rmm` accepts `--ops` with a comma-separated operation list. The benchmark harness only builds the query pools needed by the selected operations, so subset runs avoid much of the setup cost: - -```sh -./build/release/bench_rmm --ops=rank1,select1 --benchmark_out=rmm_rank_select.json --benchmark_out_format=json +#endif ``` -By default, RmM benchmarks step through sizes by powers of two. Use `--per_octave=` for finer sampling between adjacent powers of two, or `--explicit_sizes=` for an exact size list. - -Google Benchmark filters are also used to limit RmM setup when `--ops` is not provided: - -```sh -./build/release/bench_rmm --benchmark_filter='^rank1$' --benchmark_out=rmm_rank1.json --benchmark_out_format=json -``` - -For comparison with range min-max tree implementation from [sdsl-lite](https://github.com/simongog/sdsl-lite), use the third-party benchmark preset. This defines `SDSL_SUPPORT` and builds `bench_rmm_sdsl`: - -```bash -cmake --preset benchmarks-third-party -cmake --build --preset benchmarks-third-party -sudo cpupower frequency-set --governor performance -./build/release-third-party/bench_rmm_sdsl --benchmark_out=rmm_bench_sdsl.json -``` - -For visualization, write the JSON output to a file using `--benchmark_out=` (e.g. `./build/release/bench_rmm --benchmark_out=rmm_bench.json`) and plot it with `scripts/plot_rmm.py` (add `--sdsl-json rmm_bench_sdsl.json` for per-operation sdsl-lite comparison plots). For size-scaled tree plots, use: - -```sh -python3 scripts/plot_size_benchmarks.py rmm_bench.json -o graphs/rmm_size.png --size-key N -``` +The remaining `*_impl()` methods complete the `RmMBase` contract; benchmarks +call the inherited public facade, not SDSL-specific methods. --- ## Example Usage ```cpp -#include -#include -#include - -using namespace pixie; - -int main() { - std::vector bits = {0b101101}; // 6 bits - BitVector bv(bits, 6); - - std::cout << "bv: " << bv.to_string() << "\n"; // "101101" - std::cout << "rank(4): " << bv.rank(4) << "\n"; // number of ones in first 4 bits - std::cout << "select(2): " << bv.select(2) << "\n"; // position of 2nd one-bit -} -``` - -```cpp -#include +#include #include -#include -#include -#include +#include -using namespace pixie; +#include int main() { - // root - // ├─ A - // │ ├─ a1 - // │ └─ a2 - // ├─ B - // └─ C - // └─ c1 - std::string bits = "11101001011000"; - std::vector words((bits.size() + 63) / 64); - for (std::size_t i = 0; i < bits.size(); ++i) { - if (bits[i] == '1') { - words[i / 64] |= std::uint64_t{1} << (i % 64); - } - } - - // RmMTree is non-owning: keep words alive and immutable while using t. - RmMTree t(words, bits.size()); + const std::array text = {2, 0, 1, 2, 1, 0}; + pixie::WaveletTree tree(3, std::span(text)); - std::cout << "close(1): " << t.close(1) << "\n"; // expected 6 (A) - std::cout << "open(3): " << t.open(3) << "\n"; // expected 2 (a1) - std::cout << "enclose(1): " << t.enclose(1) << "\n"; // expected 0 (root) + const auto ones_before_five = tree.rank(1, 5); // 2 + const auto second_two = tree.select(2, 2); // 3 + const auto segment = tree.get_segment(1, 4); // {0, 1, 2} } ``` ---- - -## References - - - [1] Laws et al., *SPIDER: Improved Succinct Rank and Select Performance* [SPIDER](https://github.com/williams-cs/spider) +## License - - [2] Kurpicz, *Engineering compact data structures for rank and select queries on bit vectors* [pasta-toolbox/bit\_vector](https://github.com/pasta-toolbox/bit_vector) +Copyright 2026 Pixie contributors. ---- +Pixie is licensed under the [Apache License 2.0](LICENSE). Optional +third-party benchmark and backend integrations retain their own licenses. diff --git a/academic/AGENTS.md b/academic/AGENTS.md index 200c8ff..5d1a407 100644 --- a/academic/AGENTS.md +++ b/academic/AGENTS.md @@ -110,13 +110,9 @@ To create a new paper: copy `papers/sample-paper/` directory and modify. Ensure ## Key References -The following works are foundational to Pixie's design: - -- **SPIDER** (Williams & Kurlin, 2023): Reference for BitVector implementation -- **pasta-toolbox**: Reference implementation for comparison benchmarks -- **sdsl-lite** (Gog et al., 2014): Predecessor succinct data structure library -- **Navarro (2016)**: "Compact Data Structures" -- comprehensive textbook -- **Jacobson (1989)**: Original PhD thesis on succinct graph representations +`bibliography/references.bib` is the canonical bibliography for Pixie. Add and +update references there rather than embedding paper citations in library +descriptions or project guidance. ## Guidelines for AI Agents diff --git a/academic/bibliography/references.bib b/academic/bibliography/references.bib index a60c569..b217ae3 100644 --- a/academic/bibliography/references.bib +++ b/academic/bibliography/references.bib @@ -134,14 +134,34 @@ @misc{Malkovsky2026Pixie note = {GitHub repository} } -% --- BitVector / SPIDER --- - -@misc{spider2023, - author = {Williams, Christopher S. and Kurlin, Vitaliy}, - title = {SPIDER: Succinct Positional Index for Data-Intensive Efficient Retrieval}, - year = {2023}, - url = {https://github.com/williams-cs/spider}, - note = {GitHub repository} +% --- Rank/Select Engineering --- + +@inproceedings{Laws2024, + author = {Laws, Matthew D. and Bliven, Jocelyn and Conklin, Kit and Laalai, Elyes and McCauley, Samuel and Sturdevant, Zach S.}, + title = {{SPIDER: Improved Succinct Rank and Select Performance}}, + booktitle = {22nd International Symposium on Experimental Algorithms (SEA 2024)}, + pages = {21:1--21:18}, + series = {Leibniz International Proceedings in Informatics (LIPIcs)}, + volume = {301}, + editor = {Liberti, Leo}, + publisher = {Schloss Dagstuhl -- Leibniz-Zentrum f{\"u}r Informatik}, + address = {Dagstuhl, Germany}, + year = {2024}, + doi = {10.4230/LIPIcs.SEA.2024.21}, + url = {https://drops.dagstuhl.de/entities/document/10.4230/LIPIcs.SEA.2024.21} +} + +@inproceedings{Kurpicz2022, + author = {Kurpicz, Florian}, + title = {Engineering Compact Data Structures for Rank and Select Queries on Bit Vectors}, + booktitle = {String Processing and Information Retrieval}, + series = {Lecture Notes in Computer Science}, + volume = {13617}, + pages = {257--272}, + publisher = {Springer}, + year = {2022}, + doi = {10.1007/978-3-031-20643-6_19}, + url = {https://doi.org/10.1007/978-3-031-20643-6_19} } % --- RMQ / Range Minimum Query --- diff --git a/agentic/cpp b/agentic/cpp index 82337ae..c723c1e 160000 --- a/agentic/cpp +++ b/agentic/cpp @@ -1 +1 @@ -Subproject commit 82337ae84a17d3e294130c523724828e6de026f0 +Subproject commit c723c1e81354f3d40ad00766f432bf66e5c27a2b diff --git a/agentic/local/cpp/skills/benchmarks/EXAMPLES.md b/agentic/local/cpp/skills/benchmarks/EXAMPLES.md index daba5f7..d034038 100644 --- a/agentic/local/cpp/skills/benchmarks/EXAMPLES.md +++ b/agentic/local/cpp/skills/benchmarks/EXAMPLES.md @@ -3,12 +3,62 @@ Use these notes with `agentic/cpp/skills/benchmarks/SKILL.md` for Pixie's local benchmark binaries and reporting conventions. +## Rank/Select Benchmarking + +The primary rank/select benchmark binary is: + +```bash +./build/benchmarks/rank_select_benchmarks +``` + +It registers construction (`rank_select_build_both_*`) and query rows +(`rank1`, `rank0`, `select1`, `select0`) for independently generated packed bit +vectors with expected 12.5%, 50%, and 87.5% one-fill. Query rows build the +support and query pool before timing. The support owns only index metadata, so +the external bit sequence is reported separately as `input_bytes`. Every row +constructs `RankSelectSupport` with `SelectSupport::kBoth`, so its memory +counters include select1 and select0 metadata. + +Use a pinned Release run over the documented table sizes: + +```bash +taskset -c 0 ./build/benchmarks/rank_select_benchmarks \ + --benchmark_filter='^rank_select_(rank1|rank0|select1|select0|build_both)_(12p5|50|87p5)/N:(1024|67108864|17179869184)(/|$)' \ + --benchmark_report_aggregates_only=true \ + --benchmark_display_aggregates_only=true +``` + +Rows append Google Benchmark's `min_time` and warmup suffixes, so filters must +accept `(/|$)` after the size. Build rows report CPU milliseconds; query rows +report CPU nanoseconds. Both expose `aux_bytes`, `aux_mib`, and +`aux_bits_per_input_bit` for metadata owned by the support. + +### FNBP Source Rows + +`rank_select_fnbp_*` generates the Ferrada-Navarro balanced-parentheses source +used by the Cartesian RMQ backends. It covers `2^10` through `2^30` bits, +including a large-source case with 16,384 rank superblocks. + +```bash +taskset -c 0 ./build/benchmarks/rank_select_benchmarks \ + --benchmark_filter='^rank_select_fnbp_(build_both|rank1|rank0|select1|select0)/N:(1024|16384|262144|4194304|67108864|1073741824)(/|$)' \ + --benchmark_report_aggregates_only=true \ + --benchmark_display_aggregates_only=true +``` + +Do not commit raw benchmark JSON or ad hoc result tables for stable +implementations. A user-requested current snapshot may live in the relevant +implementation catalog header; keep only rounded results and reproducible +methodology, align tables visually in source, and exclude local JSON, failed +probes, and before/after experiment history. Persist other results only for an +explicitly experimental implementation that has a registered benchmark. + ## RMQ Benchmark Tables The primary RMQ benchmark binary is usually: ```bash -./build/release-third-party/bench_rmq +./build/benchmark-all-backends/rmq_benchmarks ``` When the table in `include/pixie/rmq.h` needs updating, use pinned Release @@ -18,7 +68,7 @@ paths, failed probes, or "before/after" experiment history in that header. For query rows: ```bash -taskset -c 0 ./build/release-third-party/bench_rmq \ +taskset -c 0 ./build/benchmark-all-backends/rmq_benchmarks \ --benchmark_filter='^rmq_(hybrid_btree|cartesian_hybrid_btree|cartesian_btree)/(1024|16384|262144|4194304|16777216|67108864)/' \ --benchmark_report_aggregates_only=true \ --benchmark_display_aggregates_only=true \ @@ -29,7 +79,7 @@ taskset -c 0 ./build/release-third-party/bench_rmq \ For build and memory rows: ```bash -taskset -c 0 ./build/release-third-party/bench_rmq \ +taskset -c 0 ./build/benchmark-all-backends/rmq_benchmarks \ --benchmark_filter='^rmq_build_(hybrid_btree|cartesian_hybrid_btree|cartesian_btree)/(1024|16384|262144|4194304|16777216|67108864)(/|$)' \ --benchmark_report_aggregates_only=true \ --benchmark_display_aggregates_only=true \ @@ -42,7 +92,7 @@ to registered benchmark names. Exact filters that end at the size often miss all rows; use a suffix-tolerant tail such as `(/|$)` or inspect names first: ```bash -./build/release-third-party/bench_rmq --benchmark_list_tests=true | rg cartesian_rmm +./build/benchmark-all-backends/rmq_benchmarks --benchmark_list_tests=true | rg cartesian_rmm ``` ## Memory Counters @@ -61,7 +111,7 @@ construct the structure and emit the memory counters even when query timings are not needed. For example, to measure `CartesianRmM` memory through `2^30`: ```bash -taskset -c 0 ./build/release-third-party/bench_rmq \ +taskset -c 0 ./build/benchmark-all-backends/rmq_benchmarks \ --benchmark_filter='^rmq_build_cartesian_rmm/(1024|16384|262144|4194304|16777216|67108864|268435456|1073741824)(/|$)' \ --benchmark_report_aggregates_only=true \ --benchmark_display_aggregates_only=true \ diff --git a/agentic/local/cpp/skills/optimization-experiment/EXAMPLES.md b/agentic/local/cpp/skills/optimization-experiment/EXAMPLES.md index 245f84f..7c6cd75 100644 --- a/agentic/local/cpp/skills/optimization-experiment/EXAMPLES.md +++ b/agentic/local/cpp/skills/optimization-experiment/EXAMPLES.md @@ -34,10 +34,10 @@ The `excess_min_128` experiment is a good template for future hot-path work: ## Benchmark Pattern -Prefer a diagnostic run that can explain timing changes, not just report them: +Prefer a profiling run that can explain timing changes, not just report them: ```bash -taskset -c 0 build/benchmarks-diagnostic_local/excess_positions_benchmarks \ +taskset -c 0 build/benchmarks-profile_local/excess_positions_benchmarks \ --benchmark_filter='BM_ExcessMin128(_|/|$)' \ --benchmark_repetitions=3 \ --benchmark_perf_counters=CYCLES,INSTRUCTIONS,CACHE-MISSES \ @@ -68,12 +68,12 @@ Keep this kind of report at the top of the relevant header or experiment log: RMQ benchmark snapshot, 2026-06-11. Query command: - taskset -c 0 ./build/release/bench_rmq + taskset -c 0 ./build/benchmark-all-backends/rmq_benchmarks --benchmark_filter='^(rmq_sparse_table|rmq_segment_tree|rmq_hybrid_btree|rmq_cartesian_rmm|rmq_cartesian_hybrid_btree|rmq_sdsl_sct)/' --benchmark_min_time=0.25s Build/memory command: - taskset -c 0 ./build/release/bench_rmq + taskset -c 0 ./build/benchmark-all-backends/rmq_benchmarks --benchmark_filter='^(rmq_build_sparse_table|rmq_build_segment_tree|rmq_build_hybrid_btree|rmq_build_cartesian_rmm|rmq_build_cartesian_hybrid_btree|rmq_build_sdsl_sct)/' --benchmark_min_time=0.20s diff --git a/include/pixie/bits.h b/include/pixie/bits.h index 4fb4f57..030fab4 100644 --- a/include/pixie/bits.h +++ b/include/pixie/bits.h @@ -15,6 +15,10 @@ #define PIXIE_AVX512_SUPPORT #endif +#if defined(__BMI__) && defined(__BMI2__) +#define PIXIE_BMI_SUPPORT +#endif + #ifdef __AVX2__ #define PIXIE_AVX2_SUPPORT // Lookup table for 4-bit popcount @@ -320,9 +324,18 @@ static inline uint64_t rank_512(const uint64_t* x, uint64_t count) { /** * @brief Return position of @p rank 1 bit in @p x + * @details Uses BMI/BMI2 when enabled and a portable fallback otherwise. */ static inline uint64_t select_64(uint64_t x, uint64_t rank) { +#ifdef PIXIE_BMI_SUPPORT return _tzcnt_u64(_pdep_u64(1ull << rank, x)); +#else + while (rank != 0) { + x &= x - 1; + --rank; + } + return std::countr_zero(x); +#endif } /** @@ -384,15 +397,10 @@ static inline uint64_t select_512(const uint64_t* x, uint64_t rank) { #endif } -/** - * @brief Return position of @p rank0 0 bit in @p x - * @details select_512 with bit inversion. - */ -static inline uint64_t select0_512(const uint64_t* x, uint64_t rank0) { #ifdef PIXIE_AVX512_SUPPORT - - __m512i res = _mm512_loadu_epi64(x); - res = _mm512_xor_epi64(res, _mm512_set1_epi64(-1)); +static inline uint64_t select0_512_from_inverted_words(const uint64_t* x, + uint64_t rank0, + __m512i res) { __m512i counts = _mm512_popcnt_epi64(res); __m512i prefix = counts; @@ -417,6 +425,19 @@ static inline uint64_t select0_512(const uint64_t* x, uint64_t rank0) { _mm_cvtsi128_si64(_mm512_castsi512_si128(prev_vec))); } return i * 64 + select_64(~x[i], rank0 - prev); +} +#endif + +/** + * @brief Return position of @p rank0 0 bit in @p x. + * @details select_512 with bit inversion. + */ +static inline uint64_t select0_512(const uint64_t* x, uint64_t rank0) { +#ifdef PIXIE_AVX512_SUPPORT + + __m512i res = _mm512_loadu_epi64(x); + res = _mm512_ternarylogic_epi64(res, res, res, 0x55); + return select0_512_from_inverted_words(x, rank0, res); #else diff --git a/include/pixie/cache_line.h b/include/pixie/cache_line.h deleted file mode 100644 index 596342e..0000000 --- a/include/pixie/cache_line.h +++ /dev/null @@ -1,154 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include - -#include "bit_stream.h" - -namespace pixie { - -inline constexpr std::size_t kAlignedStorageLineBytes = 64; -inline constexpr std::size_t kAlignedStorageLineBits = - kAlignedStorageLineBytes * 8; -inline constexpr std::size_t kAlignedStorageLineWords64 = - kAlignedStorageLineBytes / sizeof(std::uint64_t); -inline constexpr std::size_t kAlignedStorageLineWords16 = - kAlignedStorageLineBytes / sizeof(std::uint16_t); - -/** - * @brief A 64-byte aligned storage block. - */ -struct alignas(kAlignedStorageLineBytes) CacheLine { - std::array data{}; -}; - -static_assert(alignof(CacheLine) == kAlignedStorageLineBytes); -static_assert(sizeof(CacheLine) == kAlignedStorageLineBytes); -static_assert(kAlignedStorageLineBytes % sizeof(std::uint64_t) == 0); -static_assert(kAlignedStorageLineBytes % sizeof(std::uint16_t) == 0); - -/** - * @brief Aligned storage for 64-byte blocks. - * - * @details The constructor and resize accept a logical size in bits. Storage is - * rounded up to a full 64-byte block, and all views expose the padded capacity. - */ -class AlignedStorage { - private: - std::vector data_; - - static constexpr std::size_t LinesForBits(std::size_t bits) { - return bits / kAlignedStorageLineBits + - (bits % kAlignedStorageLineBits != 0); - } - - public: - AlignedStorage() = default; - /** - * @brief Construct storage for at least @p bits bits. - */ - explicit AlignedStorage(std::size_t bits) : data_(LinesForBits(bits)) {} - - /** - * @brief Resize storage to hold at least @p bits bits. - */ - void resize(std::size_t bits) { data_.resize(LinesForBits(bits)); } - - /** - * @brief Padded storage capacity in bits. - */ - std::size_t capacity_bits() const { - return data_.size() * kAlignedStorageLineBits; - } - - /** - * @brief Padded storage capacity in bytes. - */ - std::size_t capacity_bytes() const { - return data_.size() * kAlignedStorageLineBytes; - } - - /** - * @brief Bytes reserved by the underlying aligned cache-line buffer. - */ - std::size_t allocated_bytes() const { - return data_.capacity() * kAlignedStorageLineBytes; - } - - /** - * @brief Request that reserved storage be reduced to the current size. - */ - void shrink_to_fit() { data_.shrink_to_fit(); } - - /** - * @brief Mutable view as cache lines. - */ - std::span AsLines() { return data_; } - /** - * @brief Const view as cache lines. - */ - std::span AsConstLines() const { return data_; } - - /** - * @brief Mutable view as 64-bit words. - */ - std::span As64BitInts() { - return std::span( - reinterpret_cast(data_.data()), - data_.size() * kAlignedStorageLineWords64); - } - - /** - * @brief Const view as 64-bit words. - */ - std::span AsConst64BitInts() const { - return std::span( - reinterpret_cast(data_.data()), - data_.size() * kAlignedStorageLineWords64); - } - - /** - * @brief Mutable view as bytes. - */ - std::span AsBytes() { return std::as_writable_bytes(AsLines()); } - - /** - * @brief Const view as bytes. - */ - std::span AsConstBytes() const { - return std::as_bytes(AsConstLines()); - } - - /** - * @brief Mutable view as 16-bit words. - */ - std::span As16BitInts() { - return std::span( - reinterpret_cast(data_.data()), - data_.size() * kAlignedStorageLineWords16); - } - - /** - * @brief Const view as 16-bit words. - */ - std::span AsConst16BitInts() const { - return std::span( - reinterpret_cast(data_.data()), - data_.size() * kAlignedStorageLineWords16); - } - - /** @brief Writes bit representation of data to OutputBitStream */ - void serialize(pixie::OutputBitStream& bs) const { - bs << data_.size() * sizeof(CacheLine); - for (const CacheLine& line : data_) { - for (const std::byte& byte : line.data) { - bs << static_cast(byte); - } - } - } -}; - -} // namespace pixie diff --git a/include/pixie/experimental/excess.h b/include/pixie/experimental/excess.h index fde9f31..f5b9867 100644 --- a/include/pixie/experimental/excess.h +++ b/include/pixie/experimental/excess.h @@ -50,7 +50,7 @@ * | Split64SSE | 9.86 | 9.75 | 3.17 | 1.55 | 11.02 | * * Diagnostic run, 2026-05-30: - * taskset -c 0 build/benchmarks-diagnostic_local/excess_positions_benchmarks + * taskset -c 0 build/benchmarks-profile_local/excess_positions_benchmarks * --benchmark_filter='BM_ExcessPositions512' * --benchmark_repetitions=3 * --benchmark_perf_counters=CYCLES,INSTRUCTIONS,CACHE-MISSES diff --git a/include/pixie/io/mapped_file.h b/include/pixie/io/mapped_file.h new file mode 100644 index 0000000..64eb13c --- /dev/null +++ b/include/pixie/io/mapped_file.h @@ -0,0 +1,129 @@ +#pragma once + +#if !defined(__unix__) && !defined(__APPLE__) +#error "pixie/io/mapped_file.h requires POSIX mmap support" +#endif + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace pixie::io { + +/** + * @brief Move-only owner of a read-only POSIX memory-mapped file. + * @details The mapping remains valid after the file is unlinked. Empty files + * expose an empty byte view without creating a zero-length mapping. + */ +class MappedFile { + public: + /** + * @brief Map @p path for reading. + * @throws std::system_error if the file cannot be opened, inspected, or + * mapped. + * @throws std::length_error if the file does not fit in `std::size_t`. + */ + explicit MappedFile(const std::filesystem::path& path) { + const int descriptor = ::open(path.c_str(), O_RDONLY); + if (descriptor == -1) { + throw std::system_error(errno, std::generic_category(), + "Failed to open mapped file"); + } + + struct stat status {}; + if (::fstat(descriptor, &status) == -1) { + const int error = errno; + close_descriptor(descriptor); + throw std::system_error(error, std::generic_category(), + "Failed to inspect mapped file"); + } + + const auto file_size = static_cast(status.st_size); + if (status.st_size < 0 || + file_size > std::numeric_limits::max()) { + close_descriptor(descriptor); + throw std::length_error("Mapped file is too large"); + } + + const auto size = static_cast(file_size); + if (size == 0) { + close_descriptor(descriptor); + return; + } + + void* const mapping = + ::mmap(nullptr, size, PROT_READ, MAP_SHARED, descriptor, 0); + const int error = mapping == MAP_FAILED ? errno : 0; + close_descriptor(descriptor); + if (mapping == MAP_FAILED) { + throw std::system_error(error, std::generic_category(), + "Failed to map file"); + } + + data_ = mapping; + size_bytes_ = size; + } + + MappedFile(const MappedFile&) = delete; + MappedFile& operator=(const MappedFile&) = delete; + + /** @brief Transfer ownership of a mapping. */ + MappedFile(MappedFile&& other) noexcept + : data_(std::exchange(other.data_, nullptr)), + size_bytes_(std::exchange(other.size_bytes_, 0)) {} + + /** @brief Release this mapping and transfer ownership from @p other. */ + MappedFile& operator=(MappedFile&& other) noexcept { + if (this != &other) { + reset(); + data_ = std::exchange(other.data_, nullptr); + size_bytes_ = std::exchange(other.size_bytes_, 0); + } + return *this; + } + + /** @brief Unmap the file contents, if any. */ + ~MappedFile() { reset(); } + + /** @brief Return the mapped file contents. */ + std::span as_bytes() const noexcept { + if (data_ == nullptr) { + return {}; + } + return {static_cast(data_), size_bytes_}; + } + + /** @brief Return the mapped file size in bytes. */ + std::size_t size_bytes() const noexcept { return size_bytes_; } + + private: + static void close_descriptor(int descriptor) noexcept { + if (descriptor != -1) { + ::close(descriptor); + } + } + + void reset() noexcept { + if (data_ != nullptr) { + ::munmap(data_, size_bytes_); + data_ = nullptr; + size_bytes_ = 0; + } + } + + void* data_ = nullptr; + std::size_t size_bytes_ = 0; +}; + +} // namespace pixie::io diff --git a/include/pixie/mmap_storage.h b/include/pixie/mmap_storage.h deleted file mode 100644 index 67f7c52..0000000 --- a/include/pixie/mmap_storage.h +++ /dev/null @@ -1,85 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include - -#include "bit_stream.h" - -namespace pixie { - -class MmapFile { - private: - mio::mmap_source mmap_; - - public: - explicit MmapFile(const std::filesystem::path& path) : mmap_(path.string()) { - if (!mmap_.is_open()) { - throw std::runtime_error("Failed to mmap file"); - } - } - - MmapFile(const MmapFile&) = delete; - MmapFile& operator=(const MmapFile&) = delete; - - MmapFile(MmapFile&&) noexcept = default; - MmapFile& operator=(MmapFile&&) noexcept = default; - - std::span AsBytes() const { - return {reinterpret_cast(mmap_.data()), mmap_.size()}; - } - - size_t Size() const noexcept { return mmap_.size(); } -}; - -class MmapViewStorage { - private: - std::span data_; - - public: - MmapViewStorage() = default; - explicit MmapViewStorage(std::span data) : data_(data) {} - - void resize(size_t) { - throw std::logic_error("Cannot resize read-only mmap storage"); - } - - std::span AsConst64BitInts() const { - return {reinterpret_cast(data_.data()), data_.size() / 8}; - } - - std::span AsConst16BitInts() const { - return {reinterpret_cast(data_.data()), data_.size() / 2}; - } - - std::span AsConstBytes() const { return data_; } - - std::span As64BitInts() { - throw std::logic_error("Read-only storage"); - } - std::span As16BitInts() { - throw std::logic_error("Read-only storage"); - } - - /** @brief Writes bit representation of data to OutputBitStream */ - void serialize(pixie::OutputBitStream& bs) const { - bs << data_.size(); - for (const std::byte byte : data_) { - bs << static_cast(byte); - } - } - - static MmapViewStorage deserialize(std::span& data) { - MmapViewStorage result; - constexpr size_t length = sizeof(size_t); - size_t size; - std::memcpy(&size, data.data(), length); - result.data_ = data.subspan(length, size); - data = data.subspan(length + size); - return result; - }; -}; - -} // namespace pixie diff --git a/include/pixie/rank_select.h b/include/pixie/rank_select.h new file mode 100644 index 0000000..4414700 --- /dev/null +++ b/include/pixie/rank_select.h @@ -0,0 +1,129 @@ +#pragma once + +/** + * @file rank_select.h + * @brief Common interface for rank/select support over packed bit sequences. + * + * Include `` to use Pixie's concrete + * rank/select implementations. + */ + +#include +#include +#include +#include + +namespace pixie { + +/** + * @brief CRTP facade for immutable rank/select queries and bit access. + * + * @see `` for the available concrete + * implementations. + */ +template +class RankSelectBase { + public: + /** + * @brief Return the number of valid bits. + * @return Logical bit-vector length. + */ + std::size_t size() const { return impl().size_impl(); } + + /** + * @brief Check whether the bit vector is empty. + * @return `true` when `size() == 0`. + */ + bool empty() const { return size() == 0; } + + /** + * @brief Read a bit by zero-based position. + * @param position Position in `[0, size())`. + * @return Zero or one. + */ + int operator[](std::size_t position) const { + return impl().bit_impl(position); + } + + /** + * @brief Count one bits in the prefix `[0, end_position)`. + * @param end_position Prefix boundary in `[0, size()]`. + * @return Number of one bits in the prefix. + */ + std::uint64_t rank(std::size_t end_position) const { + return impl().rank_impl(end_position); + } + + /** + * @brief Count zero bits in the prefix `[0, end_position)`. + * @param end_position Prefix boundary in `[0, size()]`. + * @return Number of zero bits in the prefix. + */ + std::uint64_t rank0(std::size_t end_position) const { + return end_position >= size() ? size() - rank(size()) + : end_position - rank(end_position); + } + + /** + * @brief Return the position of the rank-th one bit. + * @param rank One-based rank. Rank zero returns zero for compatibility. + * @return Zero-based bit position, or `size()` if unavailable or absent. + */ + std::uint64_t select(std::size_t rank) const { + return impl().select_impl(rank); + } + + /** + * @brief Return the position of the rank-th zero bit. + * @param rank One-based rank. Rank zero returns zero for compatibility. + * @return Zero-based bit position, or `size()` if unavailable or absent. + */ + std::uint64_t select0(std::size_t rank) const { + return impl().select0_impl(rank); + } + + /** + * @brief Check whether select queries for one bits are enabled. + * @return `true` when `select()` is supported by this instance. + */ + bool supports_select1() const { return impl().supports_select1_impl(); } + + /** + * @brief Check whether select queries for zero bits are enabled. + * @return `true` when `select0()` is supported by this instance. + */ + bool supports_select0() const { return impl().supports_select0_impl(); } + + /** + * @brief Return owned index memory usage when provided by the implementation. + * @return Size of the object and its owned auxiliary storage in bytes. + */ + std::size_t memory_usage_bytes() const + requires requires(const Impl& concrete) { + { + concrete.memory_usage_bytes_impl() + } -> std::convertible_to; + } + { + return impl().memory_usage_bytes_impl(); + } + + /** + * @brief Convert the logical bits to a binary string. + * @return A string of `size()` characters containing only `0` and `1`. + */ + std::string to_string() const { + std::string result; + result.reserve(size()); + for (std::size_t i = 0; i < size(); ++i) { + result.push_back((*this)[i] ? '1' : '0'); + } + return result; + } + + private: + /** @brief Return this facade as its concrete CRTP implementation. */ + const Impl& impl() const { return static_cast(*this); } +}; + +} // namespace pixie diff --git a/include/pixie/rank_select/implementations.h b/include/pixie/rank_select/implementations.h new file mode 100644 index 0000000..02835fb --- /dev/null +++ b/include/pixie/rank_select/implementations.h @@ -0,0 +1,82 @@ +#pragma once + +/** + * @file implementations.h + * @brief All rank/select support implementations provided by Pixie. + * + * - `RankSelectSupport`: non-owning source bits with + * storage-backed rank/select metadata. + */ + +// clang-format off +/* + * Rank/select benchmark snapshot, 2026-07-13. + * + * Tables report one pinned Release pass on CPU 0, with Google Benchmark's + * 0.2 s warmup and 1.0 s minimum time. Query setup, support construction, + * and the 512 KiB query pool are outside the timed query region. Every row + * constructs RankSelectSupport with SelectSupport::kBoth, so auxiliary + * memory includes both select directions and excludes the external source. + * + * Random sources use the deterministic benchmark generator (seed 42) with the + * stated expected one-fill percentage. FNBP is the deterministic + * Ferrada-Navarro balanced-parentheses source produced by the Cartesian RMQ + * backward monotone-stack construction. FNBP rows reach 2^30; its source + * construction is linear in encoded bits. + * + * Query CPU time, ns. + * + * | source | N | rank1 | rank0 | select1 | select0 | + * | :----------- | ---: | ------: | ------: | ------: | ------: | + * | random 12.5% | 2^10 | 2.731 | 2.864 | 19.286 | 15.712 | + * | random 12.5% | 2^14 | 2.726 | 2.904 | 20.145 | 23.044 | + * | random 12.5% | 2^18 | 2.826 | 2.937 | 19.492 | 16.752 | + * | random 12.5% | 2^22 | 3.466 | 3.250 | 20.785 | 18.527 | + * | random 12.5% | 2^26 | 7.378 | 12.393 | 60.703 | 59.822 | + * | random 12.5% | 2^30 | 27.321 | 22.617 | 156.325 | 119.631 | + * | random 12.5% | 2^34 | 53.798 | 64.239 | 275.460 | 300.452 | + * | random 50% | 2^10 | 2.774 | 2.891 | 20.117 | 22.126 | + * | random 50% | 2^14 | 2.750 | 2.877 | 20.415 | 19.468 | + * | random 50% | 2^18 | 2.840 | 3.027 | 19.088 | 19.561 | + * | random 50% | 2^22 | 3.172 | 3.352 | 21.740 | 20.402 | + * | random 50% | 2^26 | 10.587 | 7.433 | 55.366 | 59.901 | + * | random 50% | 2^30 | 22.155 | 22.636 | 154.189 | 122.074 | + * | random 50% | 2^34 | 55.595 | 59.473 | 288.319 | 299.892 | + * | random 87.5% | 2^10 | 2.795 | 2.934 | 19.643 | 22.013 | + * | random 87.5% | 2^14 | 2.769 | 2.931 | 20.337 | 18.226 | + * | random 87.5% | 2^18 | 2.833 | 2.992 | 18.922 | 21.248 | + * | random 87.5% | 2^22 | 3.119 | 3.285 | 21.576 | 24.707 | + * | random 87.5% | 2^26 | 8.828 | 11.294 | 70.811 | 57.801 | + * | random 87.5% | 2^30 | 22.653 | 22.509 | 147.442 | 129.210 | + * | random 87.5% | 2^34 | 53.092 | 56.994 | 331.554 | 241.458 | + * | FNBP | 2^10 | 3.795 | 4.173 | 19.302 | 21.880 | + * | FNBP | 2^14 | 3.799 | 4.115 | 20.284 | 19.068 | + * | FNBP | 2^18 | 3.871 | 4.156 | 18.976 | 16.639 | + * | FNBP | 2^22 | 4.710 | 4.665 | 21.420 | 18.803 | + * | FNBP | 2^26 | 7.478 | 8.516 | 53.575 | 45.020 | + * | FNBP | 2^30 | 20.484 | 29.839 | 142.972 | 120.746 | + * + * Construction CPU time, ms, and owned auxiliary metadata. Random rows use + * the 50% source; this kBoth benchmark reserves combined select-sample + * capacity from the total bit count, independently of the fill ratio. + * + * | source | N | build | aux MiB | aux bits ratio | + * | :--------- | ---: | ------: | ------: | -------------: | + * | random 50% | 2^10 | 0.009 | 0.002 | 19.500 | + * | random 50% | 2^14 | 0.009 | 0.002 | 1.219 | + * | random 50% | 2^18 | 0.022 | 0.005 | 0.145 | + * | random 50% | 2^22 | 0.141 | 0.020 | 0.041 | + * | random 50% | 2^26 | 2.172 | 0.291 | 0.036 | + * | random 50% | 2^30 | 35.674 | 4.627 | 0.036 | + * | random 50% | 2^34 | 639.913 | 74.002 | 0.036 | + * | FNBP | 2^10 | 0.009 | 0.002 | 19.500 | + * | FNBP | 2^14 | 0.009 | 0.002 | 1.219 | + * | FNBP | 2^18 | 0.021 | 0.005 | 0.145 | + * | FNBP | 2^22 | 0.139 | 0.020 | 0.041 | + * | FNBP | 2^26 | 2.289 | 0.291 | 0.036 | + * | FNBP | 2^30 | 36.093 | 4.627 | 0.036 | + */ +// clang-format on + +#include +#include diff --git a/include/pixie/bitvector.h b/include/pixie/rank_select/support.h similarity index 65% rename from include/pixie/bitvector.h rename to include/pixie/rank_select/support.h index bbe3cd8..e5ea30b 100644 --- a/include/pixie/bitvector.h +++ b/include/pixie/rank_select/support.h @@ -2,17 +2,18 @@ #include #include -#include -#include +#include +#include +#include #include #include +#include #include #include #include #include #include -#include #ifdef PIXIE_DIAGNOSTICS #include @@ -21,53 +22,37 @@ namespace pixie { /** - * @brief Non-interleaved, non-owning bit vector with rank and select. + * @brief Rank/select support over an external packed bit sequence. + * @details This object does not own its source bits. It owns rank/select + * metadata through @p MetadataStorage and keeps a read-only storage view plus + * a validated 64-bit-word view of the caller-owned source. * + * Structure overview: + * - Super blocks of 2^16 bits with 64-bit ranks (0.098% overhead). + * - Basic blocks of 512 bits with 16-bit ranks (3.125% overhead). + * - Optional 64-bit select samples every 2^14 occurrences. One enabled + * direction uses at most 0.391% overhead; when both directions are enabled, + * their combined asymptotic overhead is also 0.391%. * - * @details - * This is a two-level rank/select index for a bit vector stored - * externally as - * 64-bit words. The layout follows ideas from: - * - * {1} - * "SPIDER: Improved Succinct Rank and Select Performance" - * Matthew D. Laws, - * Jocelyn Bliven, Kit Conklin, Elyes Laalai, Samuel McCauley, - * Zach S. - * Sturdevant - * https://github.com/williams-cs/spider - * - * {2} "Engineering - * compact data structures for rank and select queries on - * bit vectors" - * Kurpicz F. - * https://github.com/pasta-toolbox/bit_vector - * - * Structure - * overview: - * - Super blocks of 2^16 bits with 64-bit ranks (~0.98% - * overhead). - * - Basic blocks of 512 bits with 16-bit ranks (~3.125% - * overhead). - * - Optional select samples every 16384 bits for 1-bits, 0-bits, or both - * (~0.39% overhead per enabled direction). - * + * The rank metadata plus both select directions therefore use 3.613% + * asymptotic metadata overhead. These ratios exclude the object itself, + * sentinel entries, and 64-byte allocation rounding, which are included by + * memory_usage_bytes_impl(). * * Rank: 2 table lookups plus SIMD popcount in the 512-bit block. * * Select: - - * * - Start from a sampled super block. - * - SIMD linear scan to find the super - * block. + * - Start from a sampled super block. + * - SIMD linear scan to find the super block. * - SIMD linear scan to find the basic block. * - * This variant does - * not interleave data and index, favoring simpler scans. Rank metadata and - * enabled select samples are built in one scan over the source words. + * This variant does not interleave data and index, favoring simpler scans. + * Rank metadata and enabled select samples are built in one scan over the + * source words. */ -template -class BitVectorBase { +template +class RankSelectSupport + : public RankSelectBase> { public: /** * @brief Select directions to index during construction. @@ -92,9 +77,11 @@ class BitVectorBase { alignas(64) uint64_t delta_super[8]{}; alignas(64) uint16_t delta_basic[32]{}; - Storage super_block_rank_; // 64-bit global prefix sums - Storage basic_block_rank_; // 16-bit local prefix sums - Storage select_samples_; // 64-bit global positions + MetadataStorage super_block_rank_; // 64-bit global prefix sums + MetadataStorage basic_block_rank_; // 16-bit local prefix sums + MetadataStorage select_samples_; // 64-bit global positions + ReadOnlyStorageView source_storage_; + std::span bits_; size_t num_bits_{}; size_t padded_size_{}; size_t max_rank_{}; @@ -105,8 +92,6 @@ class BitVectorBase { SelectSupport select_support_ = SelectSupport::kNone; bool select0_samples_reversed_ = false; - std::span bits_; - static bool builds_select1(SelectSupport support) { return (static_cast(support) & static_cast(SelectSupport::kSelect1)) != 0; @@ -225,7 +210,7 @@ class BitVectorBase { } if (count >= capacity) [[unlikely]] { throw std::invalid_argument( - "BitVector one_count hint is inconsistent with input bits"); + "RankSelectSupport one_count hint is inconsistent with input bits"); } words[next] = sample; ++count; @@ -275,7 +260,7 @@ class BitVectorBase { one_count ? one_sample_capacity + zero_sample_capacity : 2 + num_bits_ / kSelectSampleFrequency; select_samples_.resize(total_samples * kWordSize); - auto samples = select_samples_.As64BitInts(); + auto samples = select_samples_.writable_words64(); select1_sample_begin_ = 0; select0_samples_reversed_ = true; writers.ones = @@ -293,7 +278,7 @@ class BitVectorBase { : (zero_count ? select_sample_count_for_rank(*zero_count) : select_sample_upper_bound(num_bits_)); select_samples_.resize(sample_capacity * kWordSize); - auto samples = select_samples_.As64BitInts(); + auto samples = select_samples_.writable_words64(); writers.shrink_after_build = !one_count; if (need_select1) { select1_sample_begin_ = 0; @@ -314,6 +299,10 @@ class BitVectorBase { select0_sample_count_ = writers.zeros.count; if (writers.zeros.reversed) { select0_sample_begin_ = writers.zeros.next + 1; + auto zero_samples = writers.zeros.words.subspan(select0_sample_begin_, + select0_sample_count_); + std::reverse(zero_samples.begin(), zero_samples.end()); + select0_samples_reversed_ = false; } if (writers.shrink_after_build) { const size_t sample_count = select1_sample_count_ != 0 @@ -325,12 +314,12 @@ class BitVectorBase { } uint64_t select1_sample(size_t sample_index) const { - auto samples = select_samples_.AsConst64BitInts(); + auto samples = select_samples_.as_words64(); return samples[select1_sample_begin_ + sample_index]; } uint64_t select0_sample(size_t sample_index) const { - auto samples = select_samples_.AsConst64BitInts(); + auto samples = select_samples_.as_words64(); if (select0_samples_reversed_) { return samples[select0_sample_begin_ + select0_sample_count_ - 1 - sample_index]; @@ -354,14 +343,14 @@ class BitVectorBase { super_block_rank_.resize(num_superblocks * 64); basic_block_rank_.resize(num_basicblocks * 16); - auto super_block_rank = super_block_rank_.As64BitInts(); - auto basic_block_rank = basic_block_rank_.As16BitInts(); + auto super_block_rank = super_block_rank_.writable_words64(); + auto basic_block_rank = basic_block_rank_.writable_words16(); const bool need_select1 = builds_select1(support); const bool need_select0 = builds_select0(support); if (one_count && *one_count > num_bits_) { throw std::invalid_argument( - "BitVector one_count hint cannot exceed num_bits"); + "RankSelectSupport one_count hint cannot exceed num_bits"); } auto select_writers = initialize_select_sample_writers(need_select1, need_select0, one_count); @@ -426,7 +415,7 @@ class BitVectorBase { * rank of the 1-bit to locate. */ uint64_t find_superblock(uint64_t rank) const { - auto super_block_rank = super_block_rank_.AsConst64BitInts(); + auto super_block_rank = super_block_rank_.as_words64(); uint64_t left = select1_sample(rank / kSelectSampleFrequency); @@ -456,7 +445,7 @@ class BitVectorBase { * rank of the 0-bit to locate. */ uint64_t find_superblock_zeros(uint64_t rank0) const { - auto super_block_rank = super_block_rank_.AsConst64BitInts(); + auto super_block_rank = super_block_rank_.as_words64(); uint64_t left = select0_sample(rank0 / kSelectSampleFrequency); @@ -495,7 +484,7 @@ class BitVectorBase { * * 4 iterations. */ uint64_t find_basicblock(uint16_t local_rank, uint64_t s_block) const { - auto basic_block_rank = basic_block_rank_.AsConst16BitInts(); + auto basic_block_rank = basic_block_rank_.as_words16(); for (size_t pos = 0; pos < kBlocksPerSuperBlock; pos += 32) { auto count = lower_bound_32x16( @@ -519,7 +508,7 @@ class BitVectorBase { * 4 iterations. */ uint64_t find_basicblock_zeros(uint16_t local_rank0, uint64_t s_block) const { - auto basic_block_rank = basic_block_rank_.AsConst16BitInts(); + auto basic_block_rank = basic_block_rank_.as_words16(); for (size_t pos = 0; pos < kBlocksPerSuperBlock; pos += 32) { auto count = lower_bound_delta_32x16( &basic_block_rank[kBlocksPerSuperBlock * s_block + pos], local_rank0, @@ -549,8 +538,8 @@ class BitVectorBase { * we backoff to find_basicblock */ uint64_t find_basicblock_is(uint16_t local_rank, uint64_t s_block) const { - auto super_block_rank = super_block_rank_.AsConst64BitInts(); - auto basic_block_rank = basic_block_rank_.AsConst16BitInts(); + auto super_block_rank = super_block_rank_.as_words64(); + auto basic_block_rank = basic_block_rank_.as_words16(); auto lower = super_block_rank[s_block]; auto upper = super_block_rank[s_block + 1]; @@ -579,33 +568,42 @@ class BitVectorBase { } /** - * @brief Interpolation search with SIMD optimization. - * @param - * local_rank0 Rank of zeros within the super block. - * @param s_block Super - * block index. - * @details - * Similar to find_basicblock_zeros but - * initial guess is based on linear - * interpolation, for random data it - * should make initial guess correct - * most of the times, we start from the - * 32 wide block with interpolation - * guess at the center, if we see that - * select result lie in lower blocks - * we backoff to find_basicblock_zeros - + * @brief Locate a zero target basic block with a validated interpolation + * estimate. + * @param local_rank0 1-based zero rank within the super block. + * @param s_block Super-block index. + * @details The estimate is checked using the existing one-prefix metadata. + * Non-uniform inputs fall back to the delta-SIMD search, so no zero-prefix + * metadata is stored. */ uint64_t find_basicblock_is_zeros(uint16_t local_rank0, uint64_t s_block) const { - auto super_block_rank = super_block_rank_.AsConst64BitInts(); - auto basic_block_rank = basic_block_rank_.AsConst16BitInts(); + auto super_block_rank = super_block_rank_.as_words64(); + auto basic_block_rank = basic_block_rank_.as_words16(); auto lower = kSuperBlockSize * s_block - super_block_rank[s_block]; auto upper = kSuperBlockSize * (s_block + 1) - super_block_rank[s_block + 1]; - uint64_t pos = kBlocksPerSuperBlock * local_rank0 / (upper - lower); + uint64_t interpolation = + kBlocksPerSuperBlock * local_rank0 / (upper - lower); + // Random data usually places the interpolation estimate in the target + // block. Validate it from existing one-prefix metadata before the SIMD + // derived-zero scan. + const uint64_t block_offset = + std::min(interpolation, kBlocksPerSuperBlock - 1); + const uint64_t block = kBlocksPerSuperBlock * s_block + block_offset; + const uint64_t zero_before = + kBasicBlockSize * block_offset - basic_block_rank[block]; + const uint64_t zero_after = block_offset + 1 == kBlocksPerSuperBlock + ? upper - lower + : kBasicBlockSize * (block_offset + 1) - + basic_block_rank[block + 1]; + if (zero_before < local_rank0 && local_rank0 <= zero_after) { + return block; + } + + uint64_t pos = interpolation; pos = pos + 16 < 32 ? 0 : (pos - 16); pos = pos > 96 ? 96 : pos; while (pos < 96) { @@ -631,15 +629,15 @@ class BitVectorBase { } public: - BitVectorBase() = default; - BitVectorBase(const BitVectorBase&) = default; - BitVectorBase(BitVectorBase&&) noexcept = default; - BitVectorBase& operator=(const BitVectorBase&) = default; - BitVectorBase& operator=(BitVectorBase&&) noexcept = default; + RankSelectSupport() = default; + RankSelectSupport(const RankSelectSupport&) = default; + RankSelectSupport(RankSelectSupport&&) noexcept = default; + RankSelectSupport& operator=(const RankSelectSupport&) = default; + RankSelectSupport& operator=(RankSelectSupport&&) noexcept = default; #ifdef PIXIE_DIAGNOSTICS struct DiagnosticsBytes { - size_t source_bitvector_bytes = 0; + size_t source_bit_sequence_bytes = 0; size_t super_block_rank_bytes = 0; size_t basic_block_rank_bytes = 0; size_t select1_samples_bytes = 0; @@ -652,14 +650,14 @@ class BitVectorBase { */ DiagnosticsBytes diagnostics_bytes() const { DiagnosticsBytes result; - result.source_bitvector_bytes = (num_bits_ + 7) / 8; - result.super_block_rank_bytes = super_block_rank_.AsConstBytes().size(); - result.basic_block_rank_bytes = basic_block_rank_.AsConstBytes().size(); + result.source_bit_sequence_bytes = (num_bits_ + 7) / 8; + result.super_block_rank_bytes = super_block_rank_.as_bytes().size(); + result.basic_block_rank_bytes = basic_block_rank_.as_bytes().size(); result.select1_samples_bytes = select1_sample_count_ * sizeof(uint64_t); result.select0_samples_bytes = select0_sample_count_ * sizeof(uint64_t); result.total_bytes = result.super_block_rank_bytes + result.basic_block_rank_bytes + - select_samples_.AsConstBytes().size(); + select_samples_.as_bytes().size(); return result; } @@ -669,15 +667,15 @@ class BitVectorBase { void memory_report() const { const auto diagnostics = diagnostics_bytes(); const double source_bytes = - static_cast(diagnostics.source_bitvector_bytes); + static_cast(diagnostics.source_bit_sequence_bytes); const auto log_bytes = [&](std::string_view label, size_t bytes) { const double percentage = source_bytes > 0.0 ? 100.0 * static_cast(bytes) / source_bytes : 0.0; - spdlog::info("BitVector {}: {} bytes ({:.2f}% of source)", label, bytes, - percentage); + spdlog::info("RankSelectSupport {}: {} bytes ({:.2f}% of source)", label, + bytes, percentage); }; - log_bytes("source_bitvector", diagnostics.source_bitvector_bytes); + log_bytes("source_bit_sequence", diagnostics.source_bit_sequence_bytes); log_bytes("super_block_rank", diagnostics.super_block_rank_bytes); log_bytes("basic_block_rank", diagnostics.basic_block_rank_bytes); log_bytes("select1_samples", diagnostics.select1_samples_bytes); @@ -686,40 +684,77 @@ class BitVectorBase { } #endif /** - * @brief Construct from an external array of 64-bit words. - * @param - * bit_vector Backing data, not owned. - * @param num_bits Number of valid - * bits in the vector. + * @brief Construct support over a read-only storage view. + * @param source_storage Packed source bytes, not owned. + * @param num_bits Number of valid source bits. * @param select_support Which select sample tables to build. Rank and rank0 * remain available in all modes. * @param one_count Optional exact number of 1-bits. When supplied, * construction can allocate select sample storage exactly. */ - explicit BitVectorBase(std::span bit_vector, - size_t num_bits, - SelectSupport select_support = SelectSupport::kBoth, - std::optional one_count = std::nullopt) - : num_bits_(std::min(num_bits, bit_vector.size() * kWordSize)), - padded_size_(((num_bits_ + kWordSize - 1) / kWordSize) * kWordSize), - bits_(bit_vector) { + explicit RankSelectSupport( + ReadOnlyStorageView source_storage, + size_t num_bits, + SelectSupport select_support = SelectSupport::kBoth, + std::optional one_count = std::nullopt) + : source_storage_(source_storage), + bits_(source_storage_.as_words64()), + num_bits_(std::min(num_bits, bits_.size() * kWordSize)), + padded_size_(((num_bits_ + kWordSize - 1) / kWordSize) * kWordSize) { build_rank_select(select_support, one_count); } + /** + * @brief Construct support over caller-owned packed 64-bit words. + * @param source_words Packed source words, not owned. + * @param num_bits Number of valid source bits. + * @param select_support Select sample tables to build. + * @param one_count Optional exact number of one bits. + */ + explicit RankSelectSupport( + std::span source_words, + size_t num_bits, + SelectSupport select_support = SelectSupport::kBoth, + std::optional one_count = std::nullopt) + : RankSelectSupport(ReadOnlyStorageView(std::as_bytes(source_words)), + num_bits, + select_support, + one_count) {} + + /** + * @brief Construct support over a Pixie storage implementation. + * @details The support retains a non-owning view. The source storage must + * remain alive and must not be resized while this object is used. + * @param source_storage Packed source storage, not owned. + * @param num_bits Number of valid source bits. + * @param select_support Select sample tables to build. + * @param one_count Optional exact number of one bits. + */ + template + explicit RankSelectSupport( + const SourceStorage& source_storage, + size_t num_bits, + SelectSupport select_support = SelectSupport::kBoth, + std::optional one_count = std::nullopt) + : RankSelectSupport(source_storage.view(), + num_bits, + select_support, + one_count) {} + /** * @brief Returns the number of valid bits. */ - size_t size() const { return num_bits_; } + size_t size_impl() const { return num_bits_; } /** * @brief Whether this index stores samples for select1 queries. */ - bool supports_select1() const { return builds_select1(select_support_); } + bool supports_select1_impl() const { return builds_select1(select_support_); } /** * @brief Whether this index stores samples for select0 queries. */ - bool supports_select0() const { return builds_select0(select_support_); } + bool supports_select0_impl() const { return builds_select0(select_support_); } /** * @brief Return owned auxiliary memory usage in bytes. @@ -727,7 +762,11 @@ class BitVectorBase { * @details Counts this rank/select object and its owned aligned metadata * buffers. The external source bit words are not owned and are excluded. */ - size_t memory_usage_bytes() const { + size_t memory_usage_bytes_impl() const + requires requires(const MetadataStorage& storage) { + storage.allocated_bytes(); + } + { return sizeof(*this) + super_block_rank_.allocated_bytes() + basic_block_rank_.allocated_bytes() + select_samples_.allocated_bytes(); @@ -738,7 +777,7 @@ class BitVectorBase { * @param pos Bit * index in [0, size()). */ - int operator[](size_t pos) const { + int bit_impl(size_t pos) const { size_t word_idx = pos / kWordSize; size_t bit_off = pos % kWordSize; @@ -751,13 +790,13 @@ class BitVectorBase { * index in [0, size()]. * @return Number of 1s in [0, pos). */ - uint64_t rank(size_t pos) const { + uint64_t rank_impl(size_t pos) const { if (pos >= num_bits_) [[unlikely]] { return max_rank_; } - auto super_block_rank = super_block_rank_.AsConst64BitInts(); - auto basic_block_rank = basic_block_rank_.AsConst16BitInts(); + auto super_block_rank = super_block_rank_.as_words64(); + auto basic_block_rank = basic_block_rank_.as_words16(); uint64_t b_block = pos / kBasicBlockSize; uint64_t s_block = pos / kSuperBlockSize; @@ -774,32 +813,18 @@ class BitVectorBase { * index in [0, size()]. * @return Number of 0s in [0, pos). */ - uint64_t rank0(size_t pos) const { - if (pos >= num_bits_) [[unlikely]] { - return num_bits_ - max_rank_; - } - return pos - rank(pos); - } - - /** - * @brief Select the position of the rank-th 1-bit (1-indexed). - * - * @param rank 1-based rank of the 1-bit to select. - * @return Bit index, or - * size() if rank is out of range. - */ - uint64_t select(size_t rank) const { + uint64_t select_impl(size_t rank) const { if (rank == 0) [[unlikely]] { return 0; } - if (!supports_select1()) [[unlikely]] { + if (!supports_select1_impl()) [[unlikely]] { return num_bits_; } if (rank > max_rank_) [[unlikely]] { return num_bits_; } - auto super_block_rank = super_block_rank_.AsConst64BitInts(); - auto basic_block_rank = basic_block_rank_.AsConst16BitInts(); + auto super_block_rank = super_block_rank_.as_words64(); + auto basic_block_rank = basic_block_rank_.as_words16(); uint64_t s_block = find_superblock(rank); rank -= super_block_rank[s_block]; @@ -815,18 +840,18 @@ class BitVectorBase { * @return Bit index, * or size() if rank0 is out of range. */ - uint64_t select0(size_t rank0) const { + uint64_t select0_impl(size_t rank0) const { if (rank0 == 0) [[unlikely]] { return 0; } - if (!supports_select0()) [[unlikely]] { + if (!supports_select0_impl()) [[unlikely]] { return num_bits_; } if (rank0 > num_bits_ - max_rank_) [[unlikely]] { return num_bits_; } - auto super_block_rank = super_block_rank_.AsConst64BitInts(); - auto basic_block_rank = basic_block_rank_.AsConst16BitInts(); + auto super_block_rank = super_block_rank_.as_words64(); + auto basic_block_rank = basic_block_rank_.as_words16(); uint64_t s_block = find_superblock_zeros(rank0); rank0 -= kSuperBlockSize * s_block - super_block_rank[s_block]; @@ -836,20 +861,6 @@ class BitVectorBase { return select_in_words(pos * kWordsPerBlock, rank0, false); } - /** - * @brief Convert to a binary string (debug helper). - */ - std::string to_string() const { - std::string result; - result.reserve(num_bits_); - - for (size_t i = 0; i < num_bits_; i++) { - result.push_back(operator[](i) ? '1' : '0'); - } - - return result; - } - void serialize(pixie::OutputBitStream& bs) const { bs << num_bits_ << padded_size_ << max_rank_ << select1_sample_begin_ << select1_sample_count_ << select0_sample_begin_ @@ -866,11 +877,13 @@ class BitVectorBase { select_samples_.serialize(bs); } - static BitVectorBase deserialize( - std::span source_bits, - std::span& data) { - BitVectorBase result; - result.bits_ = source_bits; + static RankSelectSupport deserialize(std::span source_bits, + std::span& data) + requires std::same_as + { + RankSelectSupport result; + result.source_storage_ = ReadOnlyStorageView(std::as_bytes(source_bits)); + result.bits_ = result.source_storage_.as_words64(); auto read = [&data](auto& value) { constexpr size_t length = sizeof(value); std::memcpy(&value, data.data(), length); @@ -894,205 +907,9 @@ class BitVectorBase { for (uint16_t& delta : result.delta_basic) { read(delta); } - result.super_block_rank_ = MmapViewStorage::deserialize(data); - result.basic_block_rank_ = MmapViewStorage::deserialize(data); - result.select_samples_ = MmapViewStorage::deserialize(data); - return result; - } -}; - -typedef BitVectorBase BitVector; - -/** - * @brief Interleaved, owning bit vector with rank and select. - * - * - * @details - * This variant interleaves data with local rank metadata to reduce - * cache - * misses for rank queries. It copies input bits into an interleaved - * layout. - * - * Based on: - * "SPIDER: Improved Succinct Rank and Select - * Performance" - * Matthew D. Laws, Jocelyn Bliven, Kit Conklin, Elyes Laalai, - * Samuel McCauley, - * Zach S. Sturdevant - */ -class BitVectorInterleaved { - private: - constexpr static size_t kWordSize = 64; - constexpr static size_t kSuperBlockRankIntSize = 64; - constexpr static size_t kBasicBlockRankIntSize = 16; - /** - * 496 bits data + 16 bit local rank - */ - constexpr static size_t kBasicBlockSize = 496; - /** - * 63488 = 496 * 128, so position of superblock can be obtained - * from the position of basic block by dividing on 128 or - * right shift on 7 bits which is cheaper then performing another - * division. - */ - constexpr static size_t kSuperBlockSize = 63488; - constexpr static size_t kBlocksPerSuperBlock = 128; - constexpr static size_t kWordsPerBlock = 8; - - const size_t num_bits_; - std::vector bits_interleaved; - std::vector super_block_rank_; - - class BitReader { - size_t iterator_64_ = 0; - size_t offset_size_ = 0; - size_t offset_bits_ = 0; - std::span bits_; - - public: - BitReader(std::span bits) : bits_(bits) {} - uint64_t ReadBits64(size_t num_bits) { - if (num_bits > 64) { - num_bits = 64; - } - uint64_t result = offset_bits_ & first_bits_mask(num_bits); - if (offset_size_ >= num_bits) { - offset_bits_ >>= num_bits; - offset_size_ -= num_bits; - return result; - } - uint64_t next = iterator_64_ < bits_.size() ? bits_[iterator_64_++] : 0; - result ^= (next & first_bits_mask(num_bits - offset_size_)) - << offset_size_; - offset_bits_ = (num_bits - offset_size_ == 64) - ? 0 - : next >> (num_bits - offset_size_); - offset_size_ = 64 - (num_bits - offset_size_); - return result; - } - }; - - public: - /** - * @brief Construct from an external array of 64-bit words. - * @param - * bit_vector Backing data to copy and interleave. - * @param num_bits Number - * of valid bits in the vector. - */ - explicit BitVectorInterleaved(std::span bit_vector, - size_t num_bits) - : num_bits_(std::min(num_bits, bit_vector.size() * kWordSize)) { - build_rank_interleaved(bit_vector, num_bits); - } - - /** - * @brief Mask with the lowest num bits set. - */ - static inline uint64_t first_bits_mask(size_t num) { - return num >= 64 ? UINT64_MAX : ((1llu << num) - 1); - } - - /** - * @brief Returns the number of valid bits. - */ - size_t size() const { return num_bits_; } - - /** - * @brief Returns the bit at the given position. - * @param pos Bit - * index in [0, size()). - */ - int operator[](size_t pos) const { - size_t block_id = pos / kBasicBlockSize; - size_t block_bit = pos - block_id * kBasicBlockSize; - size_t word_id = block_id * kWordsPerBlock + block_bit / kWordSize; - size_t word_bit = block_bit % kWordSize; - kWordSize; - - return (bits_interleaved[word_id] >> word_bit) & 1; - } - - /** - * @brief Build the interleaved layout and rank index. - * @param bits - * Source bit vector as 64-bit words. - * @param num_bits Number of valid - * bits in the source. - */ - void build_rank_interleaved(std::span bits, size_t num_bits) { - size_t num_superblocks = 1 + (num_bits_ - 1) / kSuperBlockSize; - super_block_rank_.resize(num_superblocks); - size_t num_basicblocks = 1 + (num_bits_ - 1) / kBasicBlockSize; - bits_interleaved.resize(num_basicblocks * (512 / kWordSize)); - - uint64_t super_block_sum = 0; - uint16_t basic_block_sum = 0; - auto bit_reader = BitReader(bits); - - for (size_t i = 0; i * kBasicBlockSize < num_bits; ++i) { - if (i % (kSuperBlockSize / kBasicBlockSize) == 0) { - super_block_sum += basic_block_sum; - super_block_rank_[i / (kSuperBlockSize / kBasicBlockSize)] = - super_block_sum; - basic_block_sum = 0; - } - bits_interleaved[i * (kWordsPerBlock) + 7] = - static_cast(basic_block_sum) << 48; - - for (size_t j = 0; j < 7 && kWordSize * (i + j) < num_bits; ++j) { - bits_interleaved[i * (kWordsPerBlock) + j] = - bit_reader.ReadBits64(std::min( - 64ull, num_bits - i * kBasicBlockSize + j * kWordSize)); - basic_block_sum += - std::popcount(bits_interleaved[i * (kWordsPerBlock) + j]); - } - if ((i + 7) * kWordSize < num_bits) { - auto v = bit_reader.ReadBits64(std::min( - 48ull, num_bits - (i * kBasicBlockSize + 7 * kWordSize))); - bits_interleaved[i * (kWordsPerBlock) + 7] ^= v; - basic_block_sum += std::popcount(v); - } - } - } - - /** - * @brief Rank of 1s up to position pos (exclusive). - * @param pos Bit - * index in [0, size()]. - * @return Number of 1s in [0, pos). - */ - uint64_t rank(size_t pos) const { - // Multiplication/devisions - uint64_t b_block = pos / kBasicBlockSize; - uint64_t s_block = b_block / kBlocksPerSuperBlock; - uint64_t b_block_pos = b_block * kWordsPerBlock; - // Super block rank - uint64_t result = super_block_rank_[s_block]; - /** - * Ok, so here's quite the important factor to load 512-bit region - * at &bits_interleaved[b_block_pos], we store local rank as 16 last - * bits of it. Prefetch should guarantee but seems like there is no - * need for it. - */ - // __builtin_prefetch(&bits_interleaved[b_block_pos]); - result += rank_512(&bits_interleaved[b_block_pos], - pos - (b_block * kBasicBlockSize)); - result += bits_interleaved[b_block_pos + 7] >> 48; - return result; - } - - /** - * @brief Convert to a binary string (debug helper). - */ - std::string to_string() const { - std::string result; - result.reserve(num_bits_); - - for (size_t i = 0; i < num_bits_; i++) { - result.push_back(operator[](i) ? '1' : '0'); - } - + result.super_block_rank_ = ReadOnlyStorageView::deserialize(data); + result.basic_block_rank_ = ReadOnlyStorageView::deserialize(data); + result.select_samples_ = ReadOnlyStorageView::deserialize(data); return result; } }; diff --git a/include/pixie/rmm_base.h b/include/pixie/rmm.h similarity index 95% rename from include/pixie/rmm_base.h rename to include/pixie/rmm.h index 9bf68dd..55995fc 100644 --- a/include/pixie/rmm_base.h +++ b/include/pixie/rmm.h @@ -1,5 +1,13 @@ #pragma once +/** + * @file rmm.h + * @brief Common interface for rank/select and range min-max indexes. + * + * Include `` to use Pixie's concrete RmM + * implementations and optional adapters. + */ + #include #include @@ -13,6 +21,9 @@ namespace pixie { * query is outside the valid domain. Balanced-parentheses navigation follows * SDSL-style zero-based parenthesis indexing: close/open/enclose accept and * return bit positions, not prefix-boundary positions. + * + * @see `` for the available concrete + * implementations. */ template class RmMBase { diff --git a/include/pixie/experimental/rmm_btree.h b/include/pixie/rmm/btree.h similarity index 99% rename from include/pixie/experimental/rmm_btree.h rename to include/pixie/rmm/btree.h index 981637b..8e9ba1c 100644 --- a/include/pixie/experimental/rmm_btree.h +++ b/include/pixie/rmm/btree.h @@ -1,9 +1,9 @@ #pragma once #include -#include #include -#include +#include +#include #include #include @@ -139,7 +139,7 @@ class RmMBTree : public RmMBase> { */ RmMBTree(std::span words, std::size_t bit_count, - BitVector::SelectSupport select_support, + RankSelectSupport<>::SelectSupport select_support, std::optional one_count = std::nullopt, std::size_t = kBlockBits) { build(words, bit_count, select_support, one_count); @@ -522,7 +522,8 @@ class RmMBTree : public RmMBase> { * @p bit_count. */ void build(std::span words, std::size_t bit_count) { - build(words, bit_count, BitVector::SelectSupport::kBoth, std::nullopt); + build(words, bit_count, RankSelectSupport<>::SelectSupport::kBoth, + std::nullopt); } /** @@ -530,7 +531,7 @@ class RmMBTree : public RmMBase> { */ void build(std::span words, std::size_t bit_count, - BitVector::SelectSupport select_support, + RankSelectSupport<>::SelectSupport select_support, std::optional one_count = std::nullopt) { const std::size_t required_words = (bit_count + 63) / 64; if (words.size() < required_words) { @@ -2234,7 +2235,7 @@ class RmMBTree : public RmMBase> { } std::span bits_; - std::optional rank_index_; + std::optional> rank_index_; std::size_t bit_count_ = 0; std::size_t block_count_ = 0; Summary top_summary_; diff --git a/include/pixie/rmm/implementations.h b/include/pixie/rmm/implementations.h new file mode 100644 index 0000000..2601e02 --- /dev/null +++ b/include/pixie/rmm/implementations.h @@ -0,0 +1,18 @@ +#pragma once + +/** + * @file implementations.h + * @brief All RmM implementations provided or adapted by Pixie. + * + * - `RmMTree`: native hierarchical RmM implementation. + * - `experimental::RmMBTree`: cache-oriented B-tree implementation. + * - `SdslRmMTree`: optional SDSL adapter when `SDSL_SUPPORT` is enabled. + */ + +#include +#include +#include + +#ifdef SDSL_SUPPORT +#include +#endif diff --git a/include/pixie/rmm_tree_sdsl.h b/include/pixie/rmm/sdsl.h similarity index 99% rename from include/pixie/rmm_tree_sdsl.h rename to include/pixie/rmm/sdsl.h index 41e50a4..60637c7 100644 --- a/include/pixie/rmm_tree_sdsl.h +++ b/include/pixie/rmm/sdsl.h @@ -1,10 +1,10 @@ #pragma once #ifndef SDSL_SUPPORT -#error "pixie/rmm_tree_sdsl.h requires SDSL_SUPPORT" +#error "pixie/rmm/sdsl.h requires SDSL_SUPPORT" #endif -#include +#include #include #include diff --git a/include/pixie/rmm_tree.h b/include/pixie/rmm/tree.h similarity index 99% rename from include/pixie/rmm_tree.h rename to include/pixie/rmm/tree.h index a522c62..e24dfe5 100644 --- a/include/pixie/rmm_tree.h +++ b/include/pixie/rmm/tree.h @@ -1,7 +1,7 @@ #pragma once #include #include -#include +#include #include #include diff --git a/include/pixie/rmq.h b/include/pixie/rmq.h index 83886e0..f525808 100644 --- a/include/pixie/rmq.h +++ b/include/pixie/rmq.h @@ -1,149 +1,108 @@ #pragma once -// clang-format off -/* - * RMQ benchmark snapshot, 2026-07-01. +/** + * @file rmq.h + * @brief Common interface for static range-minimum-query indexes. * - * Commands used for the affected hybrid variants: - * taskset -c 0 ./build/release-third-party/bench_rmq - * --benchmark_filter='^rmq_(hybrid_btree|cartesian_hybrid_btree|cartesian_btree)/(1024|16384|262144|4194304|16777216|67108864)/' - * --benchmark_report_aggregates_only=true - * --benchmark_display_aggregates_only=true - * - * taskset -c 0 ./build/release-third-party/bench_rmq - * --benchmark_filter='^rmq_build_(hybrid_btree|cartesian_hybrid_btree|cartesian_btree)/(1024|16384|262144|4194304|16777216|67108864)(/|$)' - * --benchmark_report_aggregates_only=true - * --benchmark_display_aggregates_only=true - * - * Tables report one pinned Release pass using the benchmark defaults: - * 0.2 s warmup and 2.0 s minimum time. Query times are CPU nanoseconds; - * build times are CPU milliseconds. Sparse-table rows are intentionally - * not registered above 2^22. Benchmarks use 64-bit signed integer values - * and 64-bit indexes. Owned auxiliary memory excludes external input values. - * Rows above 2^26 are focused large-RMQ measurements; unavailable columns are - * marked with "-". - * CartesianBTree is CartesianHybridBTree without the value-level top sparse overlay. - * - * Compact method names: - * sparse=SparseTable, seg=SegmentTree, hybrid=HybridBTree, - * cart-rmm=CartesianRmM, cart-hybrid=CartesianHybridBTree, - * cart-btree=CartesianBTree, sdsl-sct=SDSL SCT. - * - * Query CPU time, ns. - * - * | N | width | sparse | seg | hybrid | cart-rmm | cart-hybrid | cart-btree | sdsl-sct | - * | ---: | ----: | -----: | ----: | -----: | -------: | ----------: | ---------: | -------: | - * | 2^10 | 64 | 17.2 | 51.3 | 42.2 | 86.7 | 99.7 | 97.1 | 136.9 | - * | 2^10 | 2^10 | 24.0 | 76.4 | 49.5 | 171.8 | 138.0 | 137.2 | 245.9 | - * | ---: | ----: | -----: | ----: | -----: | -------: | ----------: | ---------: | -------: | - * | 2^14 | 64 | 19.0 | 54.1 | 45.4 | 103.4 | 118.2 | 117.2 | 166.1 | - * | 2^14 | 4096 | 20.1 | 103.8 | 65.7 | 246.2 | 115.3 | 116.9 | 512.3 | - * | 2^14 | 2^14 | 18.1 | 114.9 | 33.7 | 329.7 | 48.2 | 98.5 | 612.8 | - * | ---: | ----: | -----: | ----: | -----: | -------: | ----------: | ---------: | -------: | - * | 2^18 | 64 | 36.3 | 78.1 | 53.9 | 103.0 | 131.2 | 130.2 | 187.0 | - * | 2^18 | 4096 | 35.7 | 148.7 | 83.1 | 271.5 | 251.2 | 248.8 | 655.6 | - * | 2^18 | 2^18 | 27.8 | 196.8 | 24.7 | 452.4 | 37.3 | 285.2 | 838.7 | - * | ---: | ----: | -----: | ----: | -----: | -------: | ----------: | ---------: | -------: | - * | 2^22 | 64 | 89.3 | 170.5 | 142.6 | 115.0 | 148.3 | 149.3 | 211.0 | - * | 2^22 | 4096 | 73.2 | 310.2 | 136.0 | 305.0 | 301.7 | 300.9 | 808.0 | - * | 2^22 | 2^18 | 61.9 | 461.6 | 40.7 | 601.0 | 59.3 | 245.4 | 1078.0 | - * | 2^22 | 2^22 | 57.3 | 507.1 | 19.9 | 605.0 | 19.1 | 138.6 | 1018.0 | - * | ---: | ----: | -----: | ----: | -----: | -------: | ----------: | ---------: | -------: | - * | 2^24 | 64 | - | 193.9 | 175.8 | 170.5 | 230.0 | 218.1 | 274.9 | - * | 2^24 | 4096 | - | 365.6 | 187.1 | 470.7 | 425.6 | 441.8 | 931.9 | - * | 2^24 | 2^18 | - | 497.2 | 57.8 | 748.7 | 89.3 | 408.6 | 1276.3 | - * | 2^24 | 2^22 | - | 603.4 | 28.7 | 857.6 | 25.7 | 285.7 | 1287.3 | - * | 2^24 | 2^24 | - | 574.9 | 19.5 | 862.1 | 16.5 | 311.4 | 1251.4 | - * | ---: | ----: | -----: | ----: | -----: | -------: | ----------: | ---------: | -------: | - * | 2^26 | 64 | - | 250.4 | 201.3 | 250.0 | 276.0 | 273.8 | 559.0 | - * | 2^26 | 4096 | - | 450.0 | 237.7 | 571.0 | 497.2 | 520.6 | 1409.0 | - * | 2^26 | 2^18 | - | 678.1 | 87.8 | 895.0 | 114.7 | 487.5 | 2090.0 | - * | 2^26 | 2^22 | - | 697.8 | 40.0 | 930.0 | 36.6 | 370.2 | 2163.0 | - * | 2^26 | 2^26 | - | 698.8 | 19.1 | 1255.0 | 17.6 | 401.6 | 2299.0 | - * | ---: | ----: | -----: | ----: | -----: | -------: | ----------: | ---------: | -------: | - * | 2^28 | 64 | - | - | 300.0 | 438.8 | 451.8 | - | - | - * | 2^28 | 4096 | - | - | 372.6 | 968.1 | 803.9 | - | - | - * | 2^28 | 2^18 | - | - | 224.9 | 1516.5 | 416.6 | - | - | - * | 2^28 | 2^22 | - | - | 56.9 | 1441.8 | 77.2 | - | - | - * | 2^28 | 2^26 | - | - | 21.2 | 1449.2 | 31.7 | - | - | - * | 2^28 | 2^28 | - | - | 21.3 | 1537.7 | 19.0 | - | - | - * | ---: | ----: | -----: | ----: | -----: | -------: | ----------: | ---------: | -------: | - * | 2^30 | 64 | - | - | 334.2 | 659.2 | 656.2 | - | - | - * | 2^30 | 4096 | - | - | 408.5 | 1038.2 | 1006.4 | - | - | - * | 2^30 | 2^18 | - | - | 557.7 | 1829.2 | 927.3 | - | - | - * | 2^30 | 2^22 | - | - | 145.5 | 2487.1 | 258.8 | - | - | - * | 2^30 | 2^26 | - | - | 45.5 | 2546.9 | 82.3 | - | - | - * | 2^30 | 2^30 | - | - | 30.4 | 2103.8 | 28.5 | - | - | - * - * Build CPU time, ms. - * - * | N | sparse | seg | hybrid | cart-rmm | cart-hybrid | cart-btree | sdsl-sct | - * | ---: | ------: | -------: | -------: | --------: | ----------: | ---------: | -------: | - * | 2^10 | 0.007 | 0.001 | 0.002 | 0.014 | 0.013 | 0.012 | 0.007 | - * | ---: | ------: | -------: | -------: | --------: | ----------: | ---------: | -------: | - * | 2^14 | 1.232 | 0.014 | 0.028 | 0.137 | 0.136 | 0.121 | 0.218 | - * | ---: | ------: | -------: | -------: | --------: | ----------: | ---------: | -------: | - * | 2^18 | 30.883 | 0.279 | 0.461 | 3.372 | 3.214 | 3.053 | 2.556 | - * | ---: | ------: | -------: | -------: | --------: | ----------: | ---------: | -------: | - * | 2^22 | 715.861 | 65.124 | 8.863 | 50.700 | 55.171 | 52.499 | 47.500 | - * | ---: | ------: | -------: | -------: | --------: | ----------: | ---------: | -------: | - * | 2^24 | - | 276.132 | 35.783 | 225.624 | 226.634 | 204.719 | 170.930 | - * | ---: | ------: | -------: | -------: | --------: | ----------: | ---------: | -------: | - * | 2^26 | - | 1281.908 | 138.485 | 821.000 | 874.314 | 849.984 | 813.000 | - * | ---: | ------: | -------: | -------: | --------: | ----------: | ---------: | -------: | - * | 2^28 | - | - | 586.827 | 3730.628 | 3347.897 | - | - | - * | ---: | ------: | -------: | -------: | --------: | ----------: | ---------: | -------: | - * | 2^30 | - | - | 2324.537 | 15002.269 | 16070.000 | - | - | - * - * Owned auxiliary memory, MiB. - * - * | N | sparse | seg | hybrid | cart-rmm | cart-hybrid | cart-btree | sdsl-sct | - * | ---: | ------: | -------: | ------: | -------: | ----------: | ---------: | -------: | - * | 2^10 | 0.071 | 0.016 | 0.001 | 0.004 | 0.014 | 0.014 | 0.001 | - * | ---: | ------: | -------: | ------: | -------: | ----------: | ---------: | -------: | - * | 2^14 | 1.626 | 0.250 | 0.003 | 0.008 | 0.018 | 0.017 | 0.005 | - * | ---: | ------: | -------: | ------: | -------: | ----------: | ---------: | -------: | - * | 2^18 | 34.001 | 4.000 | 0.036 | 0.077 | 0.082 | 0.079 | 0.079 | - * | ---: | ------: | -------: | ------: | -------: | ----------: | ---------: | -------: | - * | 2^22 | 672.001 | 64.000 | 0.606 | 1.180 | 1.139 | 1.053 | 1.262 | - * | ---: | ------: | -------: | ------: | -------: | ----------: | ---------: | -------: | - * | 2^24 | - | 256.000 | 2.483 | 4.711 | 4.578 | 4.171 | 5.051 | - * | ---: | ------: | -------: | ------: | -------: | ----------: | ---------: | -------: | - * | 2^26 | - | 1024.000 | 10.182 | 18.836 | 18.519 | 16.644 | 20.224 | - * | ---: | ------: | -------: | ------: | -------: | ----------: | ---------: | -------: | - * | 2^28 | - | - | 35.102 | 75.333 | 68.411 | - | - | - * | ---: | ------: | -------: | ------: | -------: | ----------: | ---------: | -------: | - * | 2^30 | - | - | 134.783 | 301.325 | 267.979 | - | - | + * Include `` for the implementation catalog and + * current benchmark comparison. + */ + +#include +#include +#include + +namespace pixie::rmq { + +/** + * @brief CRTP facade for static range-minimum-query indexes. * - * Owned auxiliary memory normalized by indexed value count, bits/value. + * Implementations are non-owning indexes over an external random-access array. + * Queries use half-open zero-based ranges `[left, right)` and return the first + * position attaining the minimum. Invalid or empty ranges return `npos`. * - * | N | sparse | seg | hybrid | cart-rmm | cart-hybrid | cart-btree | sdsl-sct | - * | ---: | -------: | ------: | -----: | -------: | ----------: | ---------: | -------: | - * | 2^10 | 579.188 | 128.438 | 4.500 | 29.625 | 112.938 | 112.875 | 7.312 | - * | ---: | -------: | ------: | -----: | -------: | ----------: | ---------: | -------: | - * | 2^14 | 832.262 | 128.027 | 1.293 | 3.852 | 8.977 | 8.930 | 2.770 | - * | ---: | -------: | ------: | -----: | -------: | ----------: | ---------: | -------: | - * | 2^18 | 1088.020 | 128.002 | 1.162 | 2.450 | 2.625 | 2.516 | 2.534 | - * | ---: | -------: | ------: | -----: | -------: | ----------: | ---------: | -------: | - * | 2^22 | 1344.002 | 128.000 | 1.211 | 2.360 | 2.277 | 2.105 | 2.524 | - * | ---: | -------: | ------: | -----: | -------: | ----------: | ---------: | -------: | - * | 2^24 | - | 128.000 | 1.242 | 2.356 | 2.289 | 2.086 | 2.526 | - * | ---: | -------: | ------: | -----: | -------: | ----------: | ---------: | -------: | - * | 2^26 | - | 128.000 | 1.273 | 2.354 | 2.315 | 2.081 | 2.528 | - * | ---: | -------: | ------: | -----: | -------: | ----------: | ---------: | -------: | - * | 2^28 | - | - | 1.097 | 2.354 | 2.138 | - | - | - * | ---: | -------: | ------: | -----: | -------: | ----------: | ---------: | -------: | - * | 2^30 | - | - | 1.053 | 2.354 | 2.094 | - | - | + * @see `` for the complete implementation catalog + * and current benchmark comparison. */ -// clang-format on +template +class RmqBase { + public: + /** + * @brief Sentinel returned when no valid query answer exists. + */ + static constexpr std::size_t npos = std::numeric_limits::max(); + + /** + * @brief Number of indexed values. + * + * @return The number of values covered by the underlying RMQ index. + */ + std::size_t size() const { return impl().size_impl(); } + + /** + * @brief Whether the indexed array is empty. + * + * @return `true` when `size() == 0`. + */ + bool empty() const { return size() == 0; } + + /** + * @brief Return the first minimum position in [@p left, @p right). + * + * @details The query range is half-open. Ties are resolved by returning the + * smallest position attaining the minimum. Invalid or empty ranges return + * `npos`. + * + * @param left First position in the query range. + * @param right One past the last position in the query range. + * @return Zero-based position of the first range minimum, or `npos`. + */ + std::size_t arg_min(std::size_t left, std::size_t right) const { + return impl().arg_min_impl(left, right); + } + + /** + * @brief Return the minimum value in [@p left, @p right). + * + * @details Invalid or empty ranges return a default-constructed value. + * + * @param left First position in the query range. + * @param right One past the last position in the query range. + * @return The minimum value in the half-open range, or `Value{}`. + */ + Value range_min(std::size_t left, std::size_t right) const { + const std::size_t position = arg_min(left, right); + if (position == npos) { + return Value{}; + } + return impl().value_at_impl(position); + } + + /** + * @brief Return owned auxiliary memory usage in bytes when implemented. + * + * @details Implementations opt in by exposing + * `memory_usage_bytes_impl() const`. The reported value should include the + * index object itself and heap buffers owned by the index, but not the + * external value array indexed by the RMQ. + */ + std::size_t memory_usage_bytes() const + requires requires(const Impl& concrete) { + { + concrete.memory_usage_bytes_impl() + } -> std::convertible_to; + } + { + return impl().memory_usage_bytes_impl(); + } -#include -#include -#include -#include -#include -#include + private: + /** + * @brief Return this object as its concrete CRTP implementation. + * + * @return Reference to the derived RMQ implementation. + */ + const Impl& impl() const { return static_cast(*this); } +}; -#ifdef SDSL_SUPPORT -#include -#endif +} // namespace pixie::rmq diff --git a/include/pixie/rmq/cartesian_hybrid_btree.h b/include/pixie/rmq/cartesian_hybrid_btree.h index 634adaf..c63ddbe 100644 --- a/include/pixie/rmq/cartesian_hybrid_btree.h +++ b/include/pixie/rmq/cartesian_hybrid_btree.h @@ -1,11 +1,11 @@ #pragma once #include -#include -#include #include -#include +#include +#include #include +#include #include #include @@ -103,7 +103,7 @@ class HybridBTreePlusMinusOne { */ HybridBTreePlusMinusOne(std::span bits, std::size_t depth_count, - const BitVector& rank_index) { + const RankSelectSupport<>& rank_index) { build(bits, depth_count, rank_index); } @@ -122,7 +122,7 @@ class HybridBTreePlusMinusOne { */ void build(std::span bits, std::size_t depth_count, - const BitVector& rank_index) { + const RankSelectSupport<>& rank_index) { input_bits_ = bits; depth_count_ = depth_count; external_rank_index_ = &rank_index; @@ -526,7 +526,7 @@ class HybridBTreePlusMinusOne { const std::size_t delta_count = depth_count_ - 1; if (external_rank_index_ == nullptr) { owned_rank_index_.emplace(input_bits_, delta_count, - BitVector::SelectSupport::kSelect0); + RankSelectSupport<>::SelectSupport::kSelect0); } else if (external_rank_index_->size() < delta_count) { throw std::invalid_argument( "HybridBTreePlusMinusOne external rank index is too small"); @@ -704,7 +704,7 @@ class HybridBTreePlusMinusOne { /** * @brief Return the active BP rank/select support, if one exists. */ - const BitVector* rank_index_or_null() const { + const RankSelectSupport<>* rank_index_or_null() const { return external_rank_index_ != nullptr ? external_rank_index_ : (owned_rank_index_ ? &*owned_rank_index_ : nullptr); @@ -713,7 +713,9 @@ class HybridBTreePlusMinusOne { /** * @brief Return the active BP rank/select support. */ - const BitVector& rank_index() const { return *rank_index_or_null(); } + const RankSelectSupport<>& rank_index() const { + return *rank_index_or_null(); + } /** * @brief Return the absolute open-minus-close depth at a BP depth position. @@ -1305,8 +1307,8 @@ class HybridBTreePlusMinusOne { std::span input_bits_; std::size_t depth_count_ = 0; - std::optional owned_rank_index_; - const BitVector* external_rank_index_ = nullptr; + std::optional> owned_rank_index_; + const RankSelectSupport<>* external_rank_index_ = nullptr; std::vector internal_selectors_; std::vector internal_min_positions_; std::vector internal_min_depths_; @@ -1328,8 +1330,8 @@ class HybridBTreePlusMinusOne { * * @details This class follows the same public value-RMQ specification as the * other value RMQ backends. It builds a stable Ferrada-Navarro BP - * Cartesian-tree encoding, uses `BitVector` for close-parenthesis rank/select, - * and delegates the BP-depth minimum query to + * Cartesian-tree encoding, uses `RankSelectSupport<>` for close-parenthesis + * rank/select, and delegates the BP-depth minimum query to * `detail::HybridBTreePlusMinusOne`. The BP-depth backend keeps a configurable * low-level leaf size, fixed 192-entry middle nodes with embedded minima, and * fixed 256-entry high nodes. A single coarse value-level sparse table is @@ -1577,7 +1579,7 @@ class CartesianHybridBTree if (shifted_min == npos || shifted_min == 0) { return npos; } - const BitVector& bp_index = *bp_index_; + const RankSelectSupport<>& bp_index = *bp_index_; const std::size_t answer = bp_index.rank0(shifted_min) - 1; return answer < values_.size() ? answer : npos; } @@ -1667,7 +1669,8 @@ class CartesianHybridBTree const std::span words = bp_words(); const std::span padded_words = bp_storage_words(); // TODO: try incorporating rank/select information into the tree. - bp_index_.emplace(words, bp_bit_count_, BitVector::SelectSupport::kSelect0, + bp_index_.emplace(words, bp_bit_count_, + RankSelectSupport<>::SelectSupport::kSelect0, values_.size()); bp_depth_rmq_ = BpDepthRmq(padded_words, bp_bit_count_ + 1, *bp_index_); } @@ -1691,13 +1694,15 @@ class CartesianHybridBTree /** * @brief Return mutable padded BP storage as 64-bit words. */ - std::span bp_storage_words() { return bp_bits_.As64BitInts(); } + std::span bp_storage_words() { + return bp_bits_.writable_words64(); + } /** * @brief Return padded BP storage as 64-bit words. */ std::span bp_storage_words() const { - return bp_bits_.AsConst64BitInts(); + return bp_bits_.as_words64(); } /** @@ -1895,7 +1900,7 @@ class CartesianHybridBTree std::size_t top_block_size_ = kMinTopSparseBlockSize; std::size_t top_block_count_ = 0; std::size_t top_sparse_levels_ = 0; - std::optional bp_index_; + std::optional> bp_index_; BpDepthRmq bp_depth_rmq_; }; diff --git a/include/pixie/rmq/cartesian_rmm.h b/include/pixie/rmq/cartesian_rmm.h index 8e23b20..d6ffed2 100644 --- a/include/pixie/rmq/cartesian_rmm.h +++ b/include/pixie/rmq/cartesian_rmm.h @@ -11,7 +11,7 @@ * * Focused value-RMQ rows, N=2^22 values, CPU mean across 5 repetitions. * Command shape: - * taskset -c 0 ./build/release/bench_rmq + * taskset -c 0 ./build/benchmark-all-backends/rmq_benchmarks * --benchmark_filter='^rmq_(cartesian_rmm|sdsl_sct)/4194304/' * * | max width | CartesianRmM (ns) | SdslSct (ns) | @@ -23,7 +23,7 @@ * * Raw BP rmM rows at 2^23 bits, Q=32768, CPU mean across 5 repetitions. * Command shape: - * ./build/release/bench_rmm_{btree,sdsl} + * ./build/benchmark-all-backends/rmm_{btree,sdsl}_benchmarks * --ops=range_min_query_pos,range_min_query_val * --explicit_sizes=8388608 --Q=32768 * @@ -42,7 +42,7 @@ * Historical build snapshot after select0-only BP rank/select construction, * 2026-06-13, before the RmMBTree full-block summary fast path. * Command shape: - * taskset -c 0 ./build/release/bench_rmq + * taskset -c 0 ./build/benchmark-all-backends/rmq_benchmarks * --benchmark_filter='^rmq_build_(cartesian_rmm|cartesian_hybrid_btree|sdsl_sct)/(4194304|67108864)$' * --benchmark_repetitions=5 * @@ -67,9 +67,9 @@ * generation was about 5% in both rows. */ -#include #include -#include +#include +#include #include #include @@ -131,7 +131,7 @@ class RmMPlusMinusOne { * @brief Build with an exact count of +1 delta bits. * * @details Cartesian BP construction knows this count exactly, which lets the - * nested BitVector allocate select samples without a second scan. + * nested RankSelectSupport<> allocate select samples without a second scan. */ RmMPlusMinusOne(std::span bits, std::size_t depth_count, @@ -162,8 +162,8 @@ class RmMPlusMinusOne { if (depth_count_ > static_cast(invalid_index)) { throw std::length_error("RMQ rmM btree index type is too small"); } - rmm_ = RmM(words_, depth_count_ - 1, BitVector::SelectSupport::kSelect0, - one_count); + rmm_ = RmM(words_, depth_count_ - 1, + RankSelectSupport<>::SelectSupport::kSelect0, one_count); } /** diff --git a/include/pixie/rmq/hybrid_btree.h b/include/pixie/rmq/hybrid_btree.h index 9c5f479..3c80da3 100644 --- a/include/pixie/rmq/hybrid_btree.h +++ b/include/pixie/rmq/hybrid_btree.h @@ -2,7 +2,7 @@ #include #include -#include +#include #include #include diff --git a/include/pixie/rmq/implementations.h b/include/pixie/rmq/implementations.h new file mode 100644 index 0000000..0c74c2b --- /dev/null +++ b/include/pixie/rmq/implementations.h @@ -0,0 +1,159 @@ +#pragma once + +/** + * @file implementations.h + * @brief All RMQ implementations provided by Pixie and their benchmark + * comparison. + * + * Native implementations: `SparseTable`, `SegmentTree`, `HybridBTree`, + * `CartesianRmM`, `CartesianHybridBTree`, and `CartesianBTree`. The optional + * `SdslSct` adapter is available when `SDSL_SUPPORT` is enabled. + */ + +// clang-format off +/* + * RMQ benchmark snapshot, 2026-07-01. + * + * Commands used for the affected hybrid variants: + * taskset -c 0 ./build/benchmark-all-backends/rmq_benchmarks + * --benchmark_filter='^rmq_(hybrid_btree|cartesian_hybrid_btree|cartesian_btree)/(1024|16384|262144|4194304|16777216|67108864)/' + * --benchmark_report_aggregates_only=true + * --benchmark_display_aggregates_only=true + * + * taskset -c 0 ./build/benchmark-all-backends/rmq_benchmarks + * --benchmark_filter='^rmq_build_(hybrid_btree|cartesian_hybrid_btree|cartesian_btree)/(1024|16384|262144|4194304|16777216|67108864)(/|$)' + * --benchmark_report_aggregates_only=true + * --benchmark_display_aggregates_only=true + * + * Tables report one pinned Release pass using the benchmark defaults: + * 0.2 s warmup and 2.0 s minimum time. Query times are CPU nanoseconds; + * build times are CPU milliseconds. Sparse-table rows are intentionally + * not registered above 2^22. Benchmarks use 64-bit signed integer values + * and 64-bit indexes. Owned auxiliary memory excludes external input values. + * Rows above 2^26 are focused large-RMQ measurements; unavailable columns are + * marked with "-". + * CartesianBTree is CartesianHybridBTree without the value-level top sparse overlay. + * + * Compact method names: + * sparse=SparseTable, seg=SegmentTree, hybrid=HybridBTree, + * cart-rmm=CartesianRmM, cart-hybrid=CartesianHybridBTree, + * cart-btree=CartesianBTree, sdsl-sct=SDSL SCT. + * + * Query CPU time, ns. + * + * | N | width | sparse | seg | hybrid | cart-rmm | cart-hybrid | cart-btree | sdsl-sct | + * | ---: | ----: | -----: | ----: | -----: | -------: | ----------: | ---------: | -------: | + * | 2^10 | 64 | 17.2 | 51.3 | 42.2 | 86.7 | 99.7 | 97.1 | 136.9 | + * | 2^10 | 2^10 | 24.0 | 76.4 | 49.5 | 171.8 | 138.0 | 137.2 | 245.9 | + * | ---: | ----: | -----: | ----: | -----: | -------: | ----------: | ---------: | -------: | + * | 2^14 | 64 | 19.0 | 54.1 | 45.4 | 103.4 | 118.2 | 117.2 | 166.1 | + * | 2^14 | 4096 | 20.1 | 103.8 | 65.7 | 246.2 | 115.3 | 116.9 | 512.3 | + * | 2^14 | 2^14 | 18.1 | 114.9 | 33.7 | 329.7 | 48.2 | 98.5 | 612.8 | + * | ---: | ----: | -----: | ----: | -----: | -------: | ----------: | ---------: | -------: | + * | 2^18 | 64 | 36.3 | 78.1 | 53.9 | 103.0 | 131.2 | 130.2 | 187.0 | + * | 2^18 | 4096 | 35.7 | 148.7 | 83.1 | 271.5 | 251.2 | 248.8 | 655.6 | + * | 2^18 | 2^18 | 27.8 | 196.8 | 24.7 | 452.4 | 37.3 | 285.2 | 838.7 | + * | ---: | ----: | -----: | ----: | -----: | -------: | ----------: | ---------: | -------: | + * | 2^22 | 64 | 89.3 | 170.5 | 142.6 | 115.0 | 148.3 | 149.3 | 211.0 | + * | 2^22 | 4096 | 73.2 | 310.2 | 136.0 | 305.0 | 301.7 | 300.9 | 808.0 | + * | 2^22 | 2^18 | 61.9 | 461.6 | 40.7 | 601.0 | 59.3 | 245.4 | 1078.0 | + * | 2^22 | 2^22 | 57.3 | 507.1 | 19.9 | 605.0 | 19.1 | 138.6 | 1018.0 | + * | ---: | ----: | -----: | ----: | -----: | -------: | ----------: | ---------: | -------: | + * | 2^24 | 64 | - | 193.9 | 175.8 | 170.5 | 230.0 | 218.1 | 274.9 | + * | 2^24 | 4096 | - | 365.6 | 187.1 | 470.7 | 425.6 | 441.8 | 931.9 | + * | 2^24 | 2^18 | - | 497.2 | 57.8 | 748.7 | 89.3 | 408.6 | 1276.3 | + * | 2^24 | 2^22 | - | 603.4 | 28.7 | 857.6 | 25.7 | 285.7 | 1287.3 | + * | 2^24 | 2^24 | - | 574.9 | 19.5 | 862.1 | 16.5 | 311.4 | 1251.4 | + * | ---: | ----: | -----: | ----: | -----: | -------: | ----------: | ---------: | -------: | + * | 2^26 | 64 | - | 250.4 | 201.3 | 250.0 | 276.0 | 273.8 | 559.0 | + * | 2^26 | 4096 | - | 450.0 | 237.7 | 571.0 | 497.2 | 520.6 | 1409.0 | + * | 2^26 | 2^18 | - | 678.1 | 87.8 | 895.0 | 114.7 | 487.5 | 2090.0 | + * | 2^26 | 2^22 | - | 697.8 | 40.0 | 930.0 | 36.6 | 370.2 | 2163.0 | + * | 2^26 | 2^26 | - | 698.8 | 19.1 | 1255.0 | 17.6 | 401.6 | 2299.0 | + * | ---: | ----: | -----: | ----: | -----: | -------: | ----------: | ---------: | -------: | + * | 2^28 | 64 | - | - | 300.0 | 438.8 | 451.8 | - | - | + * | 2^28 | 4096 | - | - | 372.6 | 968.1 | 803.9 | - | - | + * | 2^28 | 2^18 | - | - | 224.9 | 1516.5 | 416.6 | - | - | + * | 2^28 | 2^22 | - | - | 56.9 | 1441.8 | 77.2 | - | - | + * | 2^28 | 2^26 | - | - | 21.2 | 1449.2 | 31.7 | - | - | + * | 2^28 | 2^28 | - | - | 21.3 | 1537.7 | 19.0 | - | - | + * | ---: | ----: | -----: | ----: | -----: | -------: | ----------: | ---------: | -------: | + * | 2^30 | 64 | - | - | 334.2 | 659.2 | 656.2 | - | - | + * | 2^30 | 4096 | - | - | 408.5 | 1038.2 | 1006.4 | - | - | + * | 2^30 | 2^18 | - | - | 557.7 | 1829.2 | 927.3 | - | - | + * | 2^30 | 2^22 | - | - | 145.5 | 2487.1 | 258.8 | - | - | + * | 2^30 | 2^26 | - | - | 45.5 | 2546.9 | 82.3 | - | - | + * | 2^30 | 2^30 | - | - | 30.4 | 2103.8 | 28.5 | - | - | + * + * Build CPU time, ms. + * + * | N | sparse | seg | hybrid | cart-rmm | cart-hybrid | cart-btree | sdsl-sct | + * | ---: | ------: | -------: | -------: | --------: | ----------: | ---------: | -------: | + * | 2^10 | 0.007 | 0.001 | 0.002 | 0.014 | 0.013 | 0.012 | 0.007 | + * | ---: | ------: | -------: | -------: | --------: | ----------: | ---------: | -------: | + * | 2^14 | 1.232 | 0.014 | 0.028 | 0.137 | 0.136 | 0.121 | 0.218 | + * | ---: | ------: | -------: | -------: | --------: | ----------: | ---------: | -------: | + * | 2^18 | 30.883 | 0.279 | 0.461 | 3.372 | 3.214 | 3.053 | 2.556 | + * | ---: | ------: | -------: | -------: | --------: | ----------: | ---------: | -------: | + * | 2^22 | 715.861 | 65.124 | 8.863 | 50.700 | 55.171 | 52.499 | 47.500 | + * | ---: | ------: | -------: | -------: | --------: | ----------: | ---------: | -------: | + * | 2^24 | - | 276.132 | 35.783 | 225.624 | 226.634 | 204.719 | 170.930 | + * | ---: | ------: | -------: | -------: | --------: | ----------: | ---------: | -------: | + * | 2^26 | - | 1281.908 | 138.485 | 821.000 | 874.314 | 849.984 | 813.000 | + * | ---: | ------: | -------: | -------: | --------: | ----------: | ---------: | -------: | + * | 2^28 | - | - | 586.827 | 3730.628 | 3347.897 | - | - | + * | ---: | ------: | -------: | -------: | --------: | ----------: | ---------: | -------: | + * | 2^30 | - | - | 2324.537 | 15002.269 | 16070.000 | - | - | + * + * Owned auxiliary memory, MiB. + * + * | N | sparse | seg | hybrid | cart-rmm | cart-hybrid | cart-btree | sdsl-sct | + * | ---: | ------: | -------: | ------: | -------: | ----------: | ---------: | -------: | + * | 2^10 | 0.071 | 0.016 | 0.001 | 0.004 | 0.014 | 0.014 | 0.001 | + * | ---: | ------: | -------: | ------: | -------: | ----------: | ---------: | -------: | + * | 2^14 | 1.626 | 0.250 | 0.003 | 0.008 | 0.018 | 0.017 | 0.005 | + * | ---: | ------: | -------: | ------: | -------: | ----------: | ---------: | -------: | + * | 2^18 | 34.001 | 4.000 | 0.036 | 0.077 | 0.082 | 0.079 | 0.079 | + * | ---: | ------: | -------: | ------: | -------: | ----------: | ---------: | -------: | + * | 2^22 | 672.001 | 64.000 | 0.606 | 1.180 | 1.139 | 1.053 | 1.262 | + * | ---: | ------: | -------: | ------: | -------: | ----------: | ---------: | -------: | + * | 2^24 | - | 256.000 | 2.483 | 4.711 | 4.578 | 4.171 | 5.051 | + * | ---: | ------: | -------: | ------: | -------: | ----------: | ---------: | -------: | + * | 2^26 | - | 1024.000 | 10.182 | 18.836 | 18.519 | 16.644 | 20.224 | + * | ---: | ------: | -------: | ------: | -------: | ----------: | ---------: | -------: | + * | 2^28 | - | - | 35.102 | 75.333 | 68.411 | - | - | + * | ---: | ------: | -------: | ------: | -------: | ----------: | ---------: | -------: | + * | 2^30 | - | - | 134.783 | 301.325 | 267.979 | - | - | + * + * Owned auxiliary memory normalized by indexed value count, bits/value. + * + * | N | sparse | seg | hybrid | cart-rmm | cart-hybrid | cart-btree | sdsl-sct | + * | ---: | -------: | ------: | -----: | -------: | ----------: | ---------: | -------: | + * | 2^10 | 579.188 | 128.438 | 4.500 | 29.625 | 112.938 | 112.875 | 7.312 | + * | ---: | -------: | ------: | -----: | -------: | ----------: | ---------: | -------: | + * | 2^14 | 832.262 | 128.027 | 1.293 | 3.852 | 8.977 | 8.930 | 2.770 | + * | ---: | -------: | ------: | -----: | -------: | ----------: | ---------: | -------: | + * | 2^18 | 1088.020 | 128.002 | 1.162 | 2.450 | 2.625 | 2.516 | 2.534 | + * | ---: | -------: | ------: | -----: | -------: | ----------: | ---------: | -------: | + * | 2^22 | 1344.002 | 128.000 | 1.211 | 2.360 | 2.277 | 2.105 | 2.524 | + * | ---: | -------: | ------: | -----: | -------: | ----------: | ---------: | -------: | + * | 2^24 | - | 128.000 | 1.242 | 2.356 | 2.289 | 2.086 | 2.526 | + * | ---: | -------: | ------: | -----: | -------: | ----------: | ---------: | -------: | + * | 2^26 | - | 128.000 | 1.273 | 2.354 | 2.315 | 2.081 | 2.528 | + * | ---: | -------: | ------: | -----: | -------: | ----------: | ---------: | -------: | + * | 2^28 | - | - | 1.097 | 2.354 | 2.138 | - | - | + * | ---: | -------: | ------: | -----: | -------: | ----------: | ---------: | -------: | + * | 2^30 | - | - | 1.053 | 2.354 | 2.094 | - | - | + */ +// clang-format on + +#include +#include +#include +#include +#include +#include + +#ifdef SDSL_SUPPORT +#include +#endif diff --git a/include/pixie/rmq/rmq_base.h b/include/pixie/rmq/rmq_base.h deleted file mode 100644 index 0008b4b..0000000 --- a/include/pixie/rmq/rmq_base.h +++ /dev/null @@ -1,97 +0,0 @@ -#pragma once - -#include -#include -#include - -namespace pixie::rmq { - -/** - * @brief CRTP facade for static range-minimum-query indexes. - * - * Implementations are non-owning indexes over an external random-access array. - * Queries use half-open zero-based ranges `[left, right)` and return the first - * position attaining the minimum. Invalid or empty ranges return `npos`. - */ -template -class RmqBase { - public: - /** - * @brief Sentinel returned when no valid query answer exists. - */ - static constexpr std::size_t npos = std::numeric_limits::max(); - - /** - * @brief Number of indexed values. - * - * @return The number of values covered by the underlying RMQ index. - */ - std::size_t size() const { return impl().size_impl(); } - - /** - * @brief Whether the indexed array is empty. - * - * @return `true` when `size() == 0`. - */ - bool empty() const { return size() == 0; } - - /** - * @brief Return the first minimum position in [@p left, @p right). - * - * @details The query range is half-open. Ties are resolved by returning the - * smallest position attaining the minimum. Invalid or empty ranges return - * `npos`. - * - * @param left First position in the query range. - * @param right One past the last position in the query range. - * @return Zero-based position of the first range minimum, or `npos`. - */ - std::size_t arg_min(std::size_t left, std::size_t right) const { - return impl().arg_min_impl(left, right); - } - - /** - * @brief Return the minimum value in [@p left, @p right). - * - * @details Invalid or empty ranges return a default-constructed value. - * - * @param left First position in the query range. - * @param right One past the last position in the query range. - * @return The minimum value in the half-open range, or `Value{}`. - */ - Value range_min(std::size_t left, std::size_t right) const { - const std::size_t position = arg_min(left, right); - if (position == npos) { - return Value{}; - } - return impl().value_at_impl(position); - } - - /** - * @brief Return owned auxiliary memory usage in bytes when implemented. - * - * @details Implementations opt in by exposing - * `memory_usage_bytes_impl() const`. The reported value should include the - * index object itself and heap buffers owned by the index, but not the - * external value array indexed by the RMQ. - */ - std::size_t memory_usage_bytes() const - requires requires(const Impl& concrete) { - { - concrete.memory_usage_bytes_impl() - } -> std::convertible_to; - } - { - return impl().memory_usage_bytes_impl(); - } - - private: - /** - * @brief Return this object as its concrete CRTP implementation. - * - * @return Reference to the derived RMQ implementation. - */ - const Impl& impl() const { return static_cast(*this); } -}; - -} // namespace pixie::rmq diff --git a/include/pixie/rmq/sdsl_sct.h b/include/pixie/rmq/sdsl_sct.h index 329ad75..5322e63 100644 --- a/include/pixie/rmq/sdsl_sct.h +++ b/include/pixie/rmq/sdsl_sct.h @@ -4,7 +4,7 @@ #error "pixie/rmq/sdsl_sct.h requires SDSL_SUPPORT" #endif -#include +#include #include #include diff --git a/include/pixie/rmq/segment_tree.h b/include/pixie/rmq/segment_tree.h index 86d4125..e1557f9 100644 --- a/include/pixie/rmq/segment_tree.h +++ b/include/pixie/rmq/segment_tree.h @@ -1,7 +1,7 @@ #pragma once #include -#include +#include #include #include diff --git a/include/pixie/rmq/sparse_table.h b/include/pixie/rmq/sparse_table.h index e6b9566..f9cbea4 100644 --- a/include/pixie/rmq/sparse_table.h +++ b/include/pixie/rmq/sparse_table.h @@ -1,8 +1,8 @@ #pragma once -#include #include -#include +#include +#include #include #include diff --git a/include/pixie/rmq/utils/succinct_monotone_stack.h b/include/pixie/rmq/utils/succinct_monotone_stack.h index acd8552..1773892 100644 --- a/include/pixie/rmq/utils/succinct_monotone_stack.h +++ b/include/pixie/rmq/utils/succinct_monotone_stack.h @@ -12,7 +12,7 @@ * summaries replace the old 63-bit data/pointer-word layout. * * Command shape: - * taskset -c 0 ./build/release/bench_rmq + * taskset -c 0 ./build/benchmark-all-backends/rmq_benchmarks * --benchmark_filter='^rmq_build_(cartesian_rmm|cartesian_hybrid_btree|sdsl_sct)/(4194304|67108864)$' * --benchmark_repetitions=5 * diff --git a/include/pixie/storage.h b/include/pixie/storage.h new file mode 100644 index 0000000..0882ee6 --- /dev/null +++ b/include/pixie/storage.h @@ -0,0 +1,146 @@ +#pragma once + +/** + * @file storage.h + * @brief Common interface for byte-addressable storage. + * + * Include `` to use Pixie's concrete storage + * types. + */ + +#include + +#include +#include +#include +#include +#include + +namespace pixie { + +/** + * @brief CRTP facade for byte-addressable storage. + * + * @tparam Impl Concrete storage implementation. + */ +template +class StorageBase { + public: + /** @brief Return the exposed storage size in bytes. */ + std::size_t size_bytes() const { return impl().size_bytes_impl(); } + + /** @brief Return the exposed storage size in bits. */ + std::size_t size_bits() const { return size_bytes() * 8; } + + /** @brief Check whether the storage is empty. */ + bool empty() const { return size_bytes() == 0; } + + /** @brief Return a read-only view of all exposed bytes. */ + std::span as_bytes() const { return impl().as_bytes_impl(); } + + /** + * @brief Return a read-only view as 16-bit words. + * @throws std::invalid_argument if the data is misaligned or its size is not + * divisible by the word size. + */ + std::span as_words16() const { + return as_words(); + } + + /** + * @brief Return a read-only view as 64-bit words. + * @throws std::invalid_argument if the data is misaligned or its size is not + * divisible by the word size. + */ + std::span as_words64() const { + return as_words(); + } + + /** @brief Return a non-owning read-only view of all exposed bytes. */ + auto view() const { return impl().view_impl(0, size_bytes()); } + + /** + * @brief Return a non-owning read-only byte subrange. + * @param offset_bytes First byte in the view. + * @param count_bytes Number of bytes in the view. + * @throws std::out_of_range if the subrange is outside this storage. + */ + auto view(std::size_t offset_bytes, std::size_t count_bytes) const { + return impl().view_impl(offset_bytes, count_bytes); + } + + /** @brief Serialize the exposed byte sequence with a size prefix. */ + void serialize(OutputBitStream& stream) const { + stream << size_bytes(); + for (const std::byte byte : as_bytes()) { + stream << static_cast(byte); + } + } + + /** @brief Resize mutable storage to hold at least @p size_bits bits. */ + void resize(std::size_t size_bits) + requires requires(Impl& value) { value.resize_impl(size_bits); } + { + impl().resize_impl(size_bits); + } + + /** @brief Return writable storage bytes. */ + auto writable_bytes() + requires requires(Impl& value) { value.writable_bytes_impl(); } + { + return impl().writable_bytes_impl(); + } + + /** @brief Return writable storage as 16-bit words. */ + auto writable_words16() + requires requires(Impl& value) { value.writable_words16_impl(); } + { + return impl().writable_words16_impl(); + } + + /** @brief Return writable storage as 64-bit words. */ + auto writable_words64() + requires requires(Impl& value) { value.writable_words64_impl(); } + { + return impl().writable_words64_impl(); + } + + /** @brief Return bytes reserved by an owning storage implementation. */ + std::size_t allocated_bytes() const + requires requires(const Impl& value) { value.allocated_bytes_impl(); } + { + return impl().allocated_bytes_impl(); + } + + /** @brief Request release of unused reserved storage. */ + void shrink_to_fit() + requires requires(Impl& value) { value.shrink_to_fit_impl(); } + { + impl().shrink_to_fit_impl(); + } + + private: + /** @brief Return this facade as its concrete CRTP implementation. */ + const Impl& impl() const { return static_cast(*this); } + + /** @brief Return this facade as its mutable CRTP implementation. */ + Impl& impl() { return static_cast(*this); } + + template + std::span as_words() const { + const auto bytes = as_bytes(); + if (bytes.size() % sizeof(Word) != 0 || + reinterpret_cast(bytes.data()) % alignof(Word) != 0) { + throw std::invalid_argument("Storage is not aligned to the word type"); + } + return {reinterpret_cast(bytes.data()), + bytes.size() / sizeof(Word)}; + } +}; + +/** @brief A concrete CRTP implementation of `StorageBase`. */ +template +concept StorageImplementation = + std::derived_from>; + +} // namespace pixie diff --git a/include/pixie/storage/aligned.h b/include/pixie/storage/aligned.h new file mode 100644 index 0000000..59d5869 --- /dev/null +++ b/include/pixie/storage/aligned.h @@ -0,0 +1,111 @@ +#pragma once + +#include +#include + +#include +#include +#include +#include +#include + +namespace pixie { + +inline constexpr std::size_t kAlignedStorageLineBytes = 64; +inline constexpr std::size_t kAlignedStorageLineBits = + kAlignedStorageLineBytes * 8; +inline constexpr std::size_t kAlignedStorageLineWords64 = + kAlignedStorageLineBytes / sizeof(std::uint64_t); +inline constexpr std::size_t kAlignedStorageLineWords16 = + kAlignedStorageLineBytes / sizeof(std::uint16_t); + +/** @brief A 64-byte aligned storage block. */ +struct alignas(kAlignedStorageLineBytes) CacheLine { + std::array data{}; +}; + +static_assert(alignof(CacheLine) == kAlignedStorageLineBytes); +static_assert(sizeof(CacheLine) == kAlignedStorageLineBytes); + +/** + * @brief Owning storage rounded up to 64-byte blocks. + * + * @details Construction and resize accept a logical bit count. All exposed + * views cover the padded allocation. Resizing or destroying this object + * invalidates its read-only views. + */ +class AlignedStorage : public StorageBase { + public: + AlignedStorage() = default; + + /** @brief Construct storage for at least @p size_bits bits. */ + explicit AlignedStorage(std::size_t size_bits) + : data_(lines_for_bits(size_bits)) {} + + /** @brief Return the padded allocation size in bytes. */ + std::size_t size_bytes_impl() const { + return data_.size() * kAlignedStorageLineBytes; + } + + /** @brief Return the padded allocation as read-only bytes. */ + std::span as_bytes_impl() const { + return std::as_bytes(std::span(data_)); + } + + /** @brief Return a checked read-only byte subrange. */ + ReadOnlyStorageView view_impl(std::size_t offset_bytes, + std::size_t count_bytes) const { + if (offset_bytes > size_bytes_impl() || + count_bytes > size_bytes_impl() - offset_bytes) { + throw std::out_of_range("Storage view is outside the allocation"); + } + return ReadOnlyStorageView( + as_bytes_impl().subspan(offset_bytes, count_bytes)); + } + + /** @brief Resize to hold at least @p size_bits bits. */ + void resize_impl(std::size_t size_bits) { + data_.resize(lines_for_bits(size_bits)); + } + + /** @brief Return writable allocation bytes. */ + std::span writable_bytes_impl() { + return std::as_writable_bytes(std::span(data_)); + } + + /** @brief Return writable allocation as 16-bit words. */ + std::span writable_words16_impl() { + return {reinterpret_cast(data_.data()), + data_.size() * kAlignedStorageLineWords16}; + } + + /** @brief Return writable allocation as 64-bit words. */ + std::span writable_words64_impl() { + return {reinterpret_cast(data_.data()), + data_.size() * kAlignedStorageLineWords64}; + } + + /** @brief Return bytes reserved by the underlying vector. */ + std::size_t allocated_bytes_impl() const { + return data_.capacity() * kAlignedStorageLineBytes; + } + + /** @brief Request release of unused vector capacity. */ + void shrink_to_fit_impl() { data_.shrink_to_fit(); } + + /** @brief Return mutable cache-line blocks. */ + std::span as_lines() { return data_; } + + /** @brief Return read-only cache-line blocks. */ + std::span as_lines() const { return data_; } + + private: + static constexpr std::size_t lines_for_bits(std::size_t size_bits) { + return size_bits / kAlignedStorageLineBits + + (size_bits % kAlignedStorageLineBits != 0); + } + + std::vector data_; +}; + +} // namespace pixie diff --git a/include/pixie/storage/implementations.h b/include/pixie/storage/implementations.h new file mode 100644 index 0000000..d438c1a --- /dev/null +++ b/include/pixie/storage/implementations.h @@ -0,0 +1,12 @@ +#pragma once + +/** + * @file implementations.h + * @brief Catalog of Pixie storage implementations. + * + * - `AlignedStorage`: owning, mutable, 64-byte-aligned storage. + * - `ReadOnlyStorageView`: non-owning read-only byte storage. + */ + +#include +#include diff --git a/include/pixie/storage/read_only_view.h b/include/pixie/storage/read_only_view.h new file mode 100644 index 0000000..8c2af70 --- /dev/null +++ b/include/pixie/storage/read_only_view.h @@ -0,0 +1,62 @@ +#pragma once + +#include + +#include +#include +#include + +namespace pixie { + +/** + * @brief A non-owning, read-only view of a byte sequence. + * + * @details The caller must keep the backing storage alive and must not resize + * it while this view or a view derived from it is in use. + */ +class ReadOnlyStorageView : public StorageBase { + public: + ReadOnlyStorageView() = default; + + /** @brief Construct a view over @p data. */ + explicit ReadOnlyStorageView(std::span data) : data_(data) {} + + /** @brief Return the number of viewed bytes. */ + std::size_t size_bytes_impl() const { return data_.size(); } + + /** @brief Return the viewed bytes. */ + std::span as_bytes_impl() const { return data_; } + + /** @brief Return a checked read-only byte subrange. */ + ReadOnlyStorageView view_impl(std::size_t offset_bytes, + std::size_t count_bytes) const { + if (offset_bytes > data_.size() || + count_bytes > data_.size() - offset_bytes) { + throw std::out_of_range("Storage view is outside the backing storage"); + } + return ReadOnlyStorageView(data_.subspan(offset_bytes, count_bytes)); + } + + /** + * @brief Deserialize a size-prefixed view and advance @p data. + * @throws std::invalid_argument if the size prefix or payload is truncated. + */ + static ReadOnlyStorageView deserialize(std::span& data) { + if (data.size() < sizeof(std::size_t)) { + throw std::invalid_argument("Truncated storage size prefix"); + } + std::size_t size = 0; + std::memcpy(&size, data.data(), sizeof(size)); + if (size > data.size() - sizeof(size)) { + throw std::invalid_argument("Truncated storage payload"); + } + ReadOnlyStorageView result(data.subspan(sizeof(size), size)); + data = data.subspan(sizeof(size) + size); + return result; + } + + private: + std::span data_; +}; + +} // namespace pixie diff --git a/include/pixie/tree.h b/include/pixie/tree.h new file mode 100644 index 0000000..97b50af --- /dev/null +++ b/include/pixie/tree.h @@ -0,0 +1,137 @@ +#pragma once + +/** + * @file tree.h + * @brief Common interface and node handle for rooted ordered trees. + * + * Include `` to use Pixie's succinct tree + * encodings. + */ + +#include + +namespace pixie { + +/** + * @brief Logical node handle shared by succinct rooted-tree encodings. + */ +struct TreeNode { + /** @brief Logical node number in the encoding's traversal order. */ + std::size_t number; + + /** @brief Bit position representing the node in the succinct encoding. */ + std::size_t pos; + + /** + * @brief Construct a node handle. + * @param node_number Logical node number. + * @param representation_position Position in the succinct representation. + */ + TreeNode(std::size_t node_number, std::size_t representation_position) + : number(node_number), pos(representation_position) {} +}; + +/** + * @brief CRTP facade for rooted ordered trees. + * + * Implementations may use different succinct encodings, but expose the same + * logical navigation operations through this facade. + * + * @see `` for the available succinct encodings. + */ +template +class TreeBase { + public: + using Node = TreeNode; + + /** + * @brief Return the root node. + * @return Handle identifying the root. + */ + Node root() const { return impl().root_impl(); } + + /** + * @brief Return the number of nodes. + * @return Number of nodes in the tree. + */ + std::size_t size() const { return impl().size_impl(); } + + /** + * @brief Check whether the tree contains no nodes. + * @return `true` when `size() == 0`. + */ + bool empty() const { return size() == 0; } + + /** + * @brief Check whether @p node has no children. + * @param node Valid node handle from this tree. + * @return `true` when the node is a leaf. + */ + bool is_leaf(const Node& node) const { return impl().is_leaf_impl(node); } + + /** + * @brief Check whether @p node is the root. + * @param node Valid node handle from this tree. + * @return `true` when the node is the root. + */ + bool is_root(const Node& node) const { return impl().is_root_impl(node); } + + /** + * @brief Return the number of children of @p node. + * @param node Valid node handle from this tree. + * @return The node's child count. + */ + std::size_t degree(const Node& node) const { + return impl().degree_impl(node); + } + + /** + * @brief Return the first child of @p node. + * @param node Valid non-leaf node handle from this tree. + * @return Handle identifying the node's first child. + */ + Node first_child(const Node& node) const { + return impl().first_child_impl(node); + } + + /** + * @brief Return a child of @p node by zero-based index. + * @param node Valid node handle from this tree. + * @param index Child index in `[0, degree(node))`. + * @return Handle identifying the requested child. + */ + Node child(const Node& node, std::size_t index) const { + return impl().child_impl(node, index); + } + + /** + * @brief Return the parent of @p node. + * @param node Valid node handle from this tree. + * @return The parent handle, or the root itself when @p node is the root. + */ + Node parent(const Node& node) const { return impl().parent_impl(node); } + + /** + * @brief Check whether @p node is its parent's last child. + * @param node Valid non-root node handle from this tree. + * @return `true` when no sibling follows @p node. + */ + bool is_last_child(const Node& node) const { + return impl().is_last_child_impl(node); + } + + /** + * @brief Return the next sibling of @p node. + * @param node Valid non-root node that is not its parent's last child. + * @return Handle identifying the next sibling. + */ + Node next_sibling(const Node& node) const { + return impl().next_sibling_impl(node); + } + + private: + /** @brief Return this facade as its concrete CRTP implementation. */ + const Impl& impl() const { return static_cast(*this); } +}; + +} // namespace pixie diff --git a/include/pixie/bp_tree.h b/include/pixie/tree/bp.h similarity index 73% rename from include/pixie/bp_tree.h rename to include/pixie/tree/bp.h index f723b2c..85b10aa 100644 --- a/include/pixie/bp_tree.h +++ b/include/pixie/tree/bp.h @@ -1,8 +1,12 @@ #pragma once -#include +#include +#include -#include "utils.h" +#include +#include +#include +#include namespace pixie { @@ -11,7 +15,7 @@ namespace pixie { * representation */ template -class BPTree { +class BPTree : public TreeBase> { private: const size_t num_bits_; RMMTree rmm_; @@ -20,16 +24,7 @@ class BPTree { /** * @brief A node class of BP tree */ - struct Node { - size_t number; - size_t pos; - - /** - * @brief A node class of BP tree - */ - Node(size_t node_number, size_t bp_pos) - : number(node_number), pos(bp_pos) {} - }; + using Node = TreeNode; /** * @brief Constructor from an external array of uint64_t @@ -40,24 +35,24 @@ class BPTree { /** * @brief Returns the root node */ - Node root() const { return Node(0, 0); } + Node root_impl() const { return Node(0, 0); } /** * @brief Returns the size of the tree */ - size_t size() const { return num_bits_ / 2; } + size_t size_impl() const { return num_bits_ / 2; } /** * @brief Indicates if @p node is a leaf */ - bool is_leaf(const Node& node) const { + bool is_leaf_impl(const Node& node) const { return (node.pos + 2 == num_bits_) or rmm_.bit(node.pos + 1) == 0; } /** * @brief Indicates if @p node is a root */ - bool is_root(const Node& node) { return node.number == 0; } + bool is_root_impl(const Node& node) const { return node.number == 0; } /** * @brief Returns the number of children of a @p node @@ -65,17 +60,17 @@ class BPTree { * * TODO try make this faster */ - size_t degree(const Node& node) const { - if (is_leaf(node)) { + size_t degree_impl(const Node& node) const { + if (is_leaf_impl(node)) { return 0; } - Node child = first_child(node); + Node child = first_child_impl(node); size_t child_count = 1; while (true) { - if (is_last_child(child)) { + if (is_last_child_impl(child)) { return child_count; } - child = next_sibling(child); + child = next_sibling_impl(child); child_count++; } } @@ -83,7 +78,7 @@ class BPTree { /** * @brief Returns first child of a @p node */ - Node first_child(const Node& node) const { + Node first_child_impl(const Node& node) const { size_t pos = node.pos + 1; size_t num = node.number + 1; return Node(num, pos); @@ -96,10 +91,10 @@ class BPTree { * * TODO try make this faster */ - Node child(const Node& node, size_t i) const { - Node child = first_child(node); + Node child_impl(const Node& node, size_t i) const { + Node child = first_child_impl(node); while (i--) { - child = next_sibling(child); + child = next_sibling_impl(child); } return child; } @@ -108,9 +103,9 @@ class BPTree { * @brief Returns the parent of a @p node if @p node is not root, * else returns root */ - Node parent(const Node& node) const { + Node parent_impl(const Node& node) const { if (node.number == 0) { - return root(); + return root_impl(); } size_t pos = rmm_.enclose(node.pos); size_t num = rmm_.rank1(pos); @@ -120,7 +115,7 @@ class BPTree { /** * @brief Indicates if @p node is last child */ - bool is_last_child(const Node& node) const { + bool is_last_child_impl(const Node& node) const { size_t end = rmm_.close(node.pos); return end + 2 >= num_bits_ or rmm_.bit(end + 1) == 0; @@ -129,7 +124,7 @@ class BPTree { /** * @brief Returns next sibling of a @p node */ - Node next_sibling(const Node& node) const { + Node next_sibling_impl(const Node& node) const { size_t pos = rmm_.close(node.pos) + 1; size_t num = rmm_.rank1(pos + 1) - 1; return Node(num, pos); diff --git a/include/pixie/dfuds_tree.h b/include/pixie/tree/dfuds.h similarity index 77% rename from include/pixie/dfuds_tree.h rename to include/pixie/tree/dfuds.h index a332b54..2dfa0fa 100644 --- a/include/pixie/dfuds_tree.h +++ b/include/pixie/tree/dfuds.h @@ -1,10 +1,11 @@ #pragma once -#include +#include +#include +#include #include - -#include "utils.h" +#include namespace pixie { @@ -13,23 +14,13 @@ namespace pixie { * representation */ template -class DFUDSTree { +class DFUDSTree : public TreeBase> { private: const size_t num_bits_; RMMTree rmm_; public: - struct Node { - size_t number; - - size_t pos; - - /** - * @brief A node class of DFUDS tree - */ - Node(size_t node_number, size_t dfuds_pos) - : number(node_number), pos(dfuds_pos) {} - }; + using Node = TreeNode; /** * @brief Constructor from an external array of uint64_t @@ -43,36 +34,36 @@ class DFUDSTree { /** * @brief Returns the root node */ - static Node root() { return Node(0, 0); } + Node root_impl() const { return Node(0, 0); } /** * @brief Returns the size of the tree */ - size_t size() const { return (num_bits_ + 1) / 2; } + size_t size_impl() const { return (num_bits_ + 1) / 2; } /** * @brief Indicates if @p node is a leaf */ - bool is_leaf(const Node& node) const { + bool is_leaf_impl(const Node& node) const { return (node.pos + 1 == num_bits_) or rmm_.bit(node.pos) == 0; } /** * @brief Indicates if @p node is a root */ - bool is_root(const Node& node) const { return node.number == 0; } + bool is_root_impl(const Node& node) const { return node.number == 0; } /** * @brief Returns the number of children of a @p node */ - size_t degree(const Node& node) const { + size_t degree_impl(const Node& node) const { return rmm_.select0(node.number + 1) - node.pos; } /** * @brief Returns first child of a @p node */ - Node first_child(const Node& node) { + Node first_child_impl(const Node& node) const { size_t pos = rmm_.select0(node.number + 1); size_t num = node.number + 1; return Node(num, pos + 1); @@ -82,16 +73,18 @@ class DFUDSTree { * @brief Returns the i-th child of @p node * Indexing starts at 0 */ - Node child(const Node& node, size_t i) const { - size_t pos = rmm_.close(rmm_.select0(node.number + 1) - i) + 1; - size_t num = rmm_.rank0(pos); - return Node(num, pos); + Node child_impl(const Node& node, size_t i) const { + Node child = first_child_impl(node); + while (i--) { + child = next_sibling_impl(child); + } + return child; } /** * @brief Returns next sibling of a @p node */ - Node next_sibling(const Node& node) const { + Node next_sibling_impl(const Node& node) const { size_t end = rmm_.fwdsearch(node.pos, -1); size_t pos = end + 1; size_t num = rmm_.rank0(pos); @@ -102,9 +95,9 @@ class DFUDSTree { * @brief Returns the parent of a @p node if @p node is not root, * else returns root */ - Node parent(const Node& node) const { + Node parent_impl(const Node& node) const { if (node.number == 0) { - return root(); + return root_impl(); } size_t open = rmm_.open( node.pos - @@ -122,7 +115,7 @@ class DFUDSTree { /** * @brief Indicates if @p node is last child */ - bool is_last_child(const Node& node) const { + bool is_last_child_impl(const Node& node) const { size_t end = rmm_.fwdsearch(node.pos, -1); size_t pos = end + 1; size_t op = rmm_.open(node.pos - 1); diff --git a/include/pixie/tree/implementations.h b/include/pixie/tree/implementations.h new file mode 100644 index 0000000..46d2d63 --- /dev/null +++ b/include/pixie/tree/implementations.h @@ -0,0 +1,15 @@ +#pragma once + +/** + * @file implementations.h + * @brief All rooted ordered-tree encodings provided by Pixie. + * + * - `LoudsTree`: level-order unary degree sequence. + * - `BPTree`: balanced-parentheses encoding. + * - `DFUDSTree`: depth-first unary degree sequence. + */ + +#include +#include +#include +#include diff --git a/include/pixie/louds_tree.h b/include/pixie/tree/louds.h similarity index 69% rename from include/pixie/louds_tree.h rename to include/pixie/tree/louds.h index 2886edf..43a743b 100644 --- a/include/pixie/louds_tree.h +++ b/include/pixie/tree/louds.h @@ -1,6 +1,7 @@ #pragma once -#include +#include +#include #include #include @@ -10,21 +11,15 @@ namespace pixie { /** * @brief A node class of LOUDS tree */ -struct LoudsNode { - size_t number; - size_t pos; - - LoudsNode(size_t node_number, size_t louds_pos) - : number(node_number), pos(louds_pos) {} -}; +using LoudsNode = TreeNode; /** * @brief A tree class based on the level order unary degree sequence (LOUDS) * representation */ -class LoudsTree { +class LoudsTree : public TreeBase { private: - BitVector bv; + RankSelectSupport<> bv; public: /** @@ -36,30 +31,30 @@ class LoudsTree { /** * @brief Returns the root node */ - LoudsNode root() const { return LoudsNode(0, 0); } + LoudsNode root_impl() const { return LoudsNode(0, 0); } /** * @brief Returns the size of the tree */ - size_t size() const { return (bv.size() + 1) / 2; } + size_t size_impl() const { return (bv.size() + 1) / 2; } /** * @brief Indicates if @p node is a leaf */ - bool is_leaf(const LoudsNode& node) const { + bool is_leaf_impl(const LoudsNode& node) const { return (node.pos + 1 == bv.size()) or bv[node.pos + 1]; } /** * @brief Indicates if @p node is a root */ - bool is_root(const LoudsNode& node) const { return node.number == 0; } + bool is_root_impl(const LoudsNode& node) const { return node.number == 0; } /** * @brief Returns the number of children of a @p node */ - size_t degree(const LoudsNode& node) const { - if (is_leaf(node)) { + size_t degree_impl(const LoudsNode& node) const { + if (is_leaf_impl(node)) { return 0; } return bv.select(node.number + 2) - node.pos - 1; @@ -69,7 +64,7 @@ class LoudsTree { * @brief Returns the i-th child of @p node * Indexing starts at 0 */ - LoudsNode child(const LoudsNode& node, size_t i) const { + LoudsNode child_impl(const LoudsNode& node, size_t i) const { size_t zeros = node.pos + i + 1 - node.number; return LoudsNode(zeros, bv.select(zeros + 1)); } @@ -77,7 +72,7 @@ class LoudsTree { /** * @brief Returns first child of a @p node */ - LoudsNode first_child(const LoudsNode& node) const { + LoudsNode first_child_impl(const LoudsNode& node) const { size_t zeros = node.pos + 1 - node.number; return LoudsNode(zeros, bv.select(zeros + 1)); } @@ -86,9 +81,9 @@ class LoudsTree { * @brief Returns the parent of a @p node if @p node is not root, * else returns root */ - LoudsNode parent(const LoudsNode& node) const { + LoudsNode parent_impl(const LoudsNode& node) const { if (node.number == 0) { - return root(); + return root_impl(); } size_t zero_pos = bv.select0(node.number); size_t parent_number = zero_pos - node.number; @@ -98,7 +93,7 @@ class LoudsTree { /** * @brief Indicates if @p node is last child */ - bool is_last_child(const LoudsNode& node) const { + bool is_last_child_impl(const LoudsNode& node) const { size_t zero_pos = bv.select0(node.number); return bv[zero_pos + 1]; } @@ -106,7 +101,7 @@ class LoudsTree { /** * @brief Returns next sibling of a @p node */ - LoudsNode next_sibling(const LoudsNode& node) const { + LoudsNode next_sibling_impl(const LoudsNode& node) const { size_t sibling_number = node.number + 1; return LoudsNode(sibling_number, bv.select(sibling_number + 1)); } diff --git a/include/pixie/utils.h b/include/pixie/utils.h index 2ee6d5e..6fd781a 100644 --- a/include/pixie/utils.h +++ b/include/pixie/utils.h @@ -1,6 +1,6 @@ #pragma once -#include +#include #include #include @@ -178,6 +178,13 @@ class AdjListTree { * @brief Returns next sibling of a @p node */ AdjListNode next_sibling(const AdjListNode& node) const { - return AdjListNode(node.number + 1); + const size_t parent_number = parent(node).number; + const auto& siblings = adj[parent_number]; + for (size_t i = 1; i + 1 < siblings.size(); ++i) { + if (siblings[i] == node.number) { + return AdjListNode(siblings[i + 1]); + } + } + return node; } }; diff --git a/include/pixie/wavelet_tree.h b/include/pixie/wavelet_tree.h index 9b17ce2..1378e14 100644 --- a/include/pixie/wavelet_tree.h +++ b/include/pixie/wavelet_tree.h @@ -1,453 +1,77 @@ #pragma once -#include -#include -#include -#include -#include -#include -#include +/** + * @file wavelet_tree.h + * @brief Common interface for wavelet-tree indexes. + * + * Include `` to use Pixie's concrete + * storage-backed wavelet trees. + */ + +#include +#include #include -#include "pixie/bitvector.h" - namespace pixie { +/** @brief Construction strategy for a wavelet-tree implementation. */ enum class WaveletTreeBuildType { Standard, Huffman }; -template +/** + * @brief CRTP facade for wavelet-tree queries. + * + * @see `` for the available concrete + * implementations. + */ +template class WaveletTreeBase { - private: - using node_index_t = size_t; - static constexpr node_index_t npos = std::numeric_limits::max(); - - struct PreWaveletNode { - node_index_t parent = npos; - node_index_t left_child = npos; - node_index_t right_child = npos; - uint64_t middle; - OutputBitStream stream; - explicit PreWaveletNode(uint64_t middle) : middle(middle) {} - }; - - /** - * @brief Node of the wavelet tree - * @details - * Node with its bitvector representing division of characters corresponding - * to this node: 0 - left, 1 - right, in their original order. It also has - * indices of its children and parent for routing both up-down and bottom-up. - * - */ - struct WaveletNode { - node_index_t parent, left_child, right_child; - uint64_t middle; - Storage bit_vector_data; - BitVectorBase data; - - /** @brief Manually turns std::vector into AlignedStorage */ - static AlignedStorage align(std::vector&& data) { - AlignedStorage result(data.size() * 64); - auto view = result.As64BitInts(); - std::copy(data.begin(), data.end(), view.begin()); - return std::move(result); - } - - WaveletNode() = default; - - WaveletNode(PreWaveletNode&& node) - requires(std::same_as) - : parent(node.parent), - left_child(node.left_child), - right_child(node.right_child), - middle(node.middle), - bit_vector_data(std::move(align(node.stream.extract()))), - data(bit_vector_data.AsConst64BitInts(), node.stream.size()) {} - - /** @brief Writes a node serialization to the bit stream */ - void serialize(pixie::OutputBitStream& bs) const { - bs << parent << left_child << right_child << middle; - bit_vector_data.serialize(bs); - data.serialize(bs); - } - - /** @brief Constructs wavelet node out of the raw bytes */ - static WaveletNode deserialize(std::span& data) - requires(std::same_as) - { - WaveletNode result; - auto read = [&data](auto& value) { - constexpr size_t length = sizeof(value); - std::memcpy(&value, data.data(), length); - data = data.subspan(length); - }; - read(result.parent); - read(result.left_child); - read(result.right_child); - read(result.middle); - result.bit_vector_data = MmapViewStorage::deserialize(data); - result.data = BitVectorBase::deserialize( - result.bit_vector_data.AsConst64BitInts(), data); - return result; - } - }; - - size_t alphabet_size_, data_size_; - node_index_t root_; - std::vector nodes_; - std::vector leaves_; - std::vector permutation_, inverse_permutation_; - - /** - * @brief Recursive building of the nodes - * - * @tparam F get_middle function typename - * @param begin Begin of the characters segment - * @param end End of the characters segment - * @param parent Node index of the parent node - * @param get_middle Offset of separating cut from the segment beginning if - * there is precomputed tree structure and npos otherwise - * @param prefix_sum Span with i-th value equal to number of characters less - * than i in the original data - * @param nodes Resulting vector of PreWaveletNodes - * @return Index of the node built - * - */ - template - node_index_t build_node(size_t begin, - size_t end, - node_index_t parent, - const F& get_middle, - std::span prefix_sum, - std::vector& nodes) - requires(std::same_as) - { - if (end - begin == 1) { - leaves_[begin] = parent; - return npos; - } - if (prefix_sum[end] == prefix_sum[begin]) { - for (size_t symbol = begin; symbol < end; symbol++) { - leaves_[symbol] = parent; - } - return npos; - } - - node_index_t result = nodes.size(); - size_t middle = get_middle(result); - middle = begin + (middle == npos ? (end - begin) / 2 : middle); - - nodes.emplace_back(middle); - nodes[result].stream.reserve(prefix_sum[end] - prefix_sum[begin]); - nodes[result].parent = parent; - nodes[result].left_child = - build_node(begin, middle, result, get_middle, prefix_sum, nodes); - nodes[result].right_child = - build_node(middle, end, result, get_middle, prefix_sum, nodes); - - return result; - } - - /** - * @brief Recursively copies segment of the original data corresponding to the - * node - * - * @param node Index of the node - * @param begin Begin of the segment - * @param end End of the segment - * @param dst Destination of the copy - * @param tmp Temporary memory buffer - * - * @details - * Copies left and right childs segments to the tmp and then builds up target - * segment in the dst - * - */ - void copy_segment_content(node_index_t node, - size_t begin, - size_t end, - std::span dst, - std::span tmp) const { - if (begin == end) { - return; - } - const size_t rank = nodes_[node].data.rank(begin), rank0 = begin - rank; - const size_t right = nodes_[node].data.rank(end) - rank, - left = (end - begin) - right; - - if (nodes_[node].left_child == npos) { - std::fill_n(tmp.begin(), static_cast(left), - inverse_permutation_[nodes_[node].middle - 1]); - } else { - copy_segment_content(nodes_[node].left_child, rank0, rank0 + left, - tmp.subspan(0, left), dst.subspan(0, left)); - } - if (nodes_[node].right_child == npos) { - std::fill(tmp.begin() + static_cast(left), tmp.end(), - inverse_permutation_[nodes_[node].middle]); - } else { - copy_segment_content(nodes_[node].right_child, rank, rank + right, - tmp.subspan(left, right), dst.subspan(left, right)); - } - - size_t j = 0, k = left; - const auto& bit_vector = nodes_[node].bit_vector_data.AsConst64BitInts(); - for (size_t i = begin; i < end; i++) { - if ((bit_vector[i / 64] >> (i % 64)) & 1) { - dst[i - begin] = tmp[k++]; - } else { - dst[i - begin] = tmp[j++]; - } - } - } - - WaveletTreeBase() = default; - public: /** - * @param alphabet_size Size of the alphabet - * @param data Original text. Its characters are from the - * range [0, alphabet_size) - * @param build_type Either Standard or Huffman. This effects on how the - * wavelet tree builds: like segment tree on trivially sorted characters or - * like in Huffman algorithm - * - * @details - * Standard: Just calls build_node - * Huffman: Reorders characters with respect to Huffman algorithm and then - * calls build_node with specific get_middle function - * + * @brief Return the number of symbols in the indexed sequence. + * @return Logical sequence length. */ - WaveletTreeBase( - size_t alphabet_size, - std::span data, - const WaveletTreeBuildType build_type = WaveletTreeBuildType::Standard) - requires(std::same_as) - : alphabet_size_(alphabet_size), - data_size_(data.size()), - leaves_(alphabet_size_, npos) { - if (alphabet_size == 0) { - root_ = npos; - return; - } - std::vector nodes; - nodes.reserve(alphabet_size_); - std::vector nodes_structure; - nodes_structure.reserve(alphabet_size_); - - if (build_type == WaveletTreeBuildType::Standard) { - permutation_.resize(alphabet_size); - inverse_permutation_.resize(alphabet_size); - std::iota(permutation_.begin(), permutation_.end(), 0); - std::iota(inverse_permutation_.begin(), inverse_permutation_.end(), 0); - nodes_structure.resize(alphabet_size_, npos); - } else { - struct Node { - size_t size, left, right; - }; - std::vector huffman_nodes(alphabet_size_, {0, 0, 0}); - for (auto symb : data) { - huffman_nodes[symb].size++; - } - - using elem_t = std::pair; - std::priority_queue, std::greater<>> queue; - for (size_t i = 0; i < alphabet_size_; i++) { - queue.emplace(huffman_nodes[i].size, i); - } - while (queue.size() >= 2) { - auto right = queue.top().second; - queue.pop(); - auto left = queue.top().second; - queue.pop(); - huffman_nodes.push_back( - {huffman_nodes[left].size + huffman_nodes[right].size, left + 1, - right + 1}); - queue.emplace(huffman_nodes.back().size, huffman_nodes.size() - 1); - } - - std::function enumerate = [&](size_t index) -> size_t { - const auto& [size, left, right] = huffman_nodes[index]; - if (left == 0 || right == 0) { - permutation_[index] = inverse_permutation_.size(); - inverse_permutation_.push_back(index); - return 1; - } - size_t ind = nodes_structure.size(), subtree = 0; - if (size > 0) { - nodes_structure.push_back(0); - } - subtree += enumerate(left - 1); - if (size > 0) { - nodes_structure[ind] = subtree; - } - subtree += enumerate(right - 1); - return subtree; - }; - - permutation_.resize(alphabet_size_); - inverse_permutation_.reserve(alphabet_size_); - enumerate(huffman_nodes.size() - 1); - } - - std::vector prefix_sum(alphabet_size + 1); - for (auto symbol : data) { - prefix_sum[permutation_[symbol] + 1]++; - } - for (size_t i = 0; i < alphabet_size_; i++) { - prefix_sum[i + 1] += prefix_sum[i]; - } - - root_ = build_node( - 0, alphabet_size_, npos, - [&](node_index_t node) { return nodes_structure[node]; }, prefix_sum, - nodes); - for (auto symbol : data) { - auto index = permutation_[symbol]; - for (node_index_t current = root_; current != npos;) { - auto& node = nodes[current]; - bool go_right = index >= node.middle; - node.stream << go_right; - if (go_right) { - current = node.right_child; - } else { - current = node.left_child; - } - } - } - nodes_.reserve(nodes.size()); - for (auto& node : nodes) { - nodes_.emplace_back(std::move(node)); - } - } + std::size_t size() const { return impl().size_impl(); } /** - * @brief Rank of specified symbol up to position pos (exclusive) - * - * @param symbol The character that the query is about - * @param pos Character index in [0, size()] - * @return Number of specified symbols in [0, pos) - * + * @brief Check whether the indexed sequence is empty. + * @return `true` when `size() == 0`. */ - size_t rank(uint64_t symbol, size_t pos) const { - if (symbol >= alphabet_size_) [[unlikely]] { - return 0; - } - symbol = permutation_[symbol]; - for (node_index_t current = root_; current != npos;) { - const WaveletNode& node = nodes_[current]; - if (symbol < node.middle) { - pos = node.data.rank0(pos); - current = node.left_child; - } else { - pos = node.data.rank(pos); - current = node.right_child; - } - } - return pos; - } + bool empty() const { return size() == 0; } /** - * @brief Select the position of the rank-th specified symbol (1-indexed) - * - * @param symbol The character that the query is about - * @param rank 1-indexed rank of specified symbol to select - * @return Symbol index, or size() if rank is out of range - * + * @brief Count occurrences of @p symbol in `[0, end_position)`. + * @param symbol Symbol in the indexed alphabet. + * @param end_position Prefix boundary in `[0, size()]`. + * @return Number of occurrences, or zero for a symbol outside the alphabet. */ - size_t select(uint64_t symbol, size_t rank) const { - if (symbol >= alphabet_size_ || data_size_ == 0) [[unlikely]] { - return data_size_; - } - symbol = permutation_[symbol]; - node_index_t current = leaves_[symbol]; - for (; current != npos; current = nodes_[current].parent) { - const WaveletNode& node = nodes_[current]; - if (symbol < node.middle) { - rank = node.data.select0(rank) + 1; - } else { - rank = node.data.select(rank) + 1; - } - } - return rank - 1; + std::size_t rank(std::uint64_t symbol, std::size_t end_position) const { + return impl().rank_impl(symbol, end_position); } /** - * @brief Accumulates the original data segment - * - * @param begin Begin of the segment - * @param end End of the segment - * @return Queried segment of data - * + * @brief Return the position of the rank-th occurrence of @p symbol. + * @param symbol Symbol in the indexed alphabet. + * @param rank One-based occurrence rank. + * @return Zero-based sequence position, or `size()` when absent. */ - std::vector get_segment(size_t begin, size_t end) const { - if (alphabet_size_ == 0 || data_size_ == 0 || begin >= end) [[unlikely]] { - return {}; - } - auto length = static_cast(end - begin); - std::vector result(2 * length); - copy_segment_content(root_, begin, end, - std::span{result.begin(), result.begin() + length}, - std::span{result.begin() + length, result.end()}); - result.resize(length); - return result; + std::size_t select(std::uint64_t symbol, std::size_t rank) const { + return impl().select_impl(symbol, rank); } /** - * @return Returns the number of characters in data - * - */ - size_t size() { return data_size_; } - - /** - * @brief Writes a wavelet tree serialization to the bit stream - * + * @brief Reconstruct the sequence range `[@p begin, @p end)`. + * @param begin First zero-based sequence position. + * @param end One past the last sequence position; must not exceed `size()`. + * @return Symbols in the requested half-open range. */ - void serialize(pixie::OutputBitStream& bs) const { - bs << alphabet_size_ << data_size_ << root_ << nodes_.size(); - for (const WaveletNode& node : nodes_) { - node.serialize(bs); - } - for (const node_index_t leaf : leaves_) { - bs << leaf; - } - for (const size_t idx : permutation_) { - bs << idx; - } + std::vector get_segment(std::size_t begin, + std::size_t end) const { + return impl().get_segment_impl(begin, end); } - static WaveletTreeBase deserialize( - std::span& data) { - WaveletTreeBase result; - auto read = [&data](auto& value) { - constexpr size_t length = sizeof(value); - std::memcpy(&value, data.data(), length); - data = data.subspan(length); - }; - read(result.alphabet_size_); - read(result.data_size_); - read(result.root_); - size_t size; - read(size); - result.nodes_.resize(size); - for (auto& node : result.nodes_) { - node = WaveletNode::deserialize(data); - } - result.leaves_.resize(result.alphabet_size_); - for (node_index_t& leaf : result.leaves_) { - read(leaf); - } - result.permutation_.resize(result.alphabet_size_); - for (size_t& index : result.permutation_) { - read(index); - } - result.inverse_permutation_.resize(result.alphabet_size_); - for (size_t i = 0; i < result.alphabet_size_; i++) { - result.inverse_permutation_[result.permutation_[i]] = i; - } - return result; - } + private: + /** @brief Return this facade as its concrete CRTP implementation. */ + const Impl& impl() const { return static_cast(*this); } }; -typedef WaveletTreeBase WaveletTree; - } // namespace pixie diff --git a/include/pixie/wavelet_tree/implementations.h b/include/pixie/wavelet_tree/implementations.h new file mode 100644 index 0000000..34510dc --- /dev/null +++ b/include/pixie/wavelet_tree/implementations.h @@ -0,0 +1,13 @@ +#pragma once + +/** + * @file implementations.h + * @brief All wavelet-tree implementations provided by Pixie. + * + * - `WaveletTreeIndex`: storage-parameterized wavelet tree. + * - `WaveletTree`: owning aligned-storage alias. + * - `WaveletTreeView`: non-owning read-only storage view alias. + */ + +#include +#include diff --git a/include/pixie/wavelet_tree/index.h b/include/pixie/wavelet_tree/index.h new file mode 100644 index 0000000..4c6a8fc --- /dev/null +++ b/include/pixie/wavelet_tree/index.h @@ -0,0 +1,453 @@ +#pragma once + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace pixie { + +template +class WaveletTreeIndex : public WaveletTreeBase> { + private: + using node_index_t = size_t; + static constexpr node_index_t npos = std::numeric_limits::max(); + + struct PreWaveletNode { + node_index_t parent = npos; + node_index_t left_child = npos; + node_index_t right_child = npos; + uint64_t middle; + OutputBitStream stream; + explicit PreWaveletNode(uint64_t middle) : middle(middle) {} + }; + + /** + * @brief Node of the wavelet tree + * @details + * Node with its bitvector representing division of characters corresponding + * to this node: 0 - left, 1 - right, in their original order. It also has + * indices of its children and parent for routing both up-down and bottom-up. + * + */ + struct WaveletNode { + node_index_t parent, left_child, right_child; + uint64_t middle; + Storage bit_vector_data; + RankSelectSupport data; + + /** @brief Manually turns std::vector into AlignedStorage */ + static AlignedStorage align(std::vector&& data) { + AlignedStorage result(data.size() * 64); + auto view = result.writable_words64(); + std::copy(data.begin(), data.end(), view.begin()); + return std::move(result); + } + + WaveletNode() = default; + + WaveletNode(PreWaveletNode&& node) + requires(std::same_as) + : parent(node.parent), + left_child(node.left_child), + right_child(node.right_child), + middle(node.middle), + bit_vector_data(std::move(align(node.stream.extract()))), + data(bit_vector_data.as_words64(), node.stream.size()) {} + + /** @brief Writes a node serialization to the bit stream */ + void serialize(pixie::OutputBitStream& bs) const { + bs << parent << left_child << right_child << middle; + bit_vector_data.serialize(bs); + data.serialize(bs); + } + + /** @brief Constructs wavelet node out of the raw bytes */ + static WaveletNode deserialize(std::span& data) + requires(std::same_as) + { + WaveletNode result; + auto read = [&data](auto& value) { + constexpr size_t length = sizeof(value); + std::memcpy(&value, data.data(), length); + data = data.subspan(length); + }; + read(result.parent); + read(result.left_child); + read(result.right_child); + read(result.middle); + result.bit_vector_data = ReadOnlyStorageView::deserialize(data); + result.data = RankSelectSupport::deserialize( + result.bit_vector_data.as_words64(), data); + return result; + } + }; + + size_t alphabet_size_, data_size_; + node_index_t root_; + std::vector nodes_; + std::vector leaves_; + std::vector permutation_, inverse_permutation_; + + /** + * @brief Recursive building of the nodes + * + * @tparam F get_middle function typename + * @param begin Begin of the characters segment + * @param end End of the characters segment + * @param parent Node index of the parent node + * @param get_middle Offset of separating cut from the segment beginning if + * there is precomputed tree structure and npos otherwise + * @param prefix_sum Span with i-th value equal to number of characters less + * than i in the original data + * @param nodes Resulting vector of PreWaveletNodes + * @return Index of the node built + * + */ + template + node_index_t build_node(size_t begin, + size_t end, + node_index_t parent, + const F& get_middle, + std::span prefix_sum, + std::vector& nodes) + requires(std::same_as) + { + if (end - begin == 1) { + leaves_[begin] = parent; + return npos; + } + if (prefix_sum[end] == prefix_sum[begin]) { + for (size_t symbol = begin; symbol < end; symbol++) { + leaves_[symbol] = parent; + } + return npos; + } + + node_index_t result = nodes.size(); + size_t middle = get_middle(result); + middle = begin + (middle == npos ? (end - begin) / 2 : middle); + + nodes.emplace_back(middle); + nodes[result].stream.reserve(prefix_sum[end] - prefix_sum[begin]); + nodes[result].parent = parent; + nodes[result].left_child = + build_node(begin, middle, result, get_middle, prefix_sum, nodes); + nodes[result].right_child = + build_node(middle, end, result, get_middle, prefix_sum, nodes); + + return result; + } + + /** + * @brief Recursively copies segment of the original data corresponding to the + * node + * + * @param node Index of the node + * @param begin Begin of the segment + * @param end End of the segment + * @param dst Destination of the copy + * @param tmp Temporary memory buffer + * + * @details + * Copies left and right childs segments to the tmp and then builds up target + * segment in the dst + * + */ + void copy_segment_content(node_index_t node, + size_t begin, + size_t end, + std::span dst, + std::span tmp) const { + if (begin == end) { + return; + } + const size_t rank = nodes_[node].data.rank(begin), rank0 = begin - rank; + const size_t right = nodes_[node].data.rank(end) - rank, + left = (end - begin) - right; + + if (nodes_[node].left_child == npos) { + std::fill_n(tmp.begin(), static_cast(left), + inverse_permutation_[nodes_[node].middle - 1]); + } else { + copy_segment_content(nodes_[node].left_child, rank0, rank0 + left, + tmp.subspan(0, left), dst.subspan(0, left)); + } + if (nodes_[node].right_child == npos) { + std::fill(tmp.begin() + static_cast(left), tmp.end(), + inverse_permutation_[nodes_[node].middle]); + } else { + copy_segment_content(nodes_[node].right_child, rank, rank + right, + tmp.subspan(left, right), dst.subspan(left, right)); + } + + size_t j = 0, k = left; + const auto& bit_vector = nodes_[node].bit_vector_data.as_words64(); + for (size_t i = begin; i < end; i++) { + if ((bit_vector[i / 64] >> (i % 64)) & 1) { + dst[i - begin] = tmp[k++]; + } else { + dst[i - begin] = tmp[j++]; + } + } + } + + WaveletTreeIndex() = default; + + public: + /** + * @param alphabet_size Size of the alphabet + * @param data Original text. Its characters are from the + * range [0, alphabet_size) + * @param build_type Either Standard or Huffman. This effects on how the + * wavelet tree builds: like segment tree on trivially sorted characters or + * like in Huffman algorithm + * + * @details + * Standard: Just calls build_node + * Huffman: Reorders characters with respect to Huffman algorithm and then + * calls build_node with specific get_middle function + * + */ + WaveletTreeIndex( + size_t alphabet_size, + std::span data, + const WaveletTreeBuildType build_type = WaveletTreeBuildType::Standard) + requires(std::same_as) + : alphabet_size_(alphabet_size), + data_size_(data.size()), + leaves_(alphabet_size_, npos) { + if (alphabet_size == 0) { + root_ = npos; + return; + } + std::vector nodes; + nodes.reserve(alphabet_size_); + std::vector nodes_structure; + nodes_structure.reserve(alphabet_size_); + + if (build_type == WaveletTreeBuildType::Standard) { + permutation_.resize(alphabet_size); + inverse_permutation_.resize(alphabet_size); + std::iota(permutation_.begin(), permutation_.end(), 0); + std::iota(inverse_permutation_.begin(), inverse_permutation_.end(), 0); + nodes_structure.resize(alphabet_size_, npos); + } else { + struct Node { + size_t size, left, right; + }; + std::vector huffman_nodes(alphabet_size_, {0, 0, 0}); + for (auto symb : data) { + huffman_nodes[symb].size++; + } + + using elem_t = std::pair; + std::priority_queue, std::greater<>> queue; + for (size_t i = 0; i < alphabet_size_; i++) { + queue.emplace(huffman_nodes[i].size, i); + } + while (queue.size() >= 2) { + auto right = queue.top().second; + queue.pop(); + auto left = queue.top().second; + queue.pop(); + huffman_nodes.push_back( + {huffman_nodes[left].size + huffman_nodes[right].size, left + 1, + right + 1}); + queue.emplace(huffman_nodes.back().size, huffman_nodes.size() - 1); + } + + std::function enumerate = [&](size_t index) -> size_t { + const auto& [size, left, right] = huffman_nodes[index]; + if (left == 0 || right == 0) { + permutation_[index] = inverse_permutation_.size(); + inverse_permutation_.push_back(index); + return 1; + } + size_t ind = nodes_structure.size(), subtree = 0; + if (size > 0) { + nodes_structure.push_back(0); + } + subtree += enumerate(left - 1); + if (size > 0) { + nodes_structure[ind] = subtree; + } + subtree += enumerate(right - 1); + return subtree; + }; + + permutation_.resize(alphabet_size_); + inverse_permutation_.reserve(alphabet_size_); + enumerate(huffman_nodes.size() - 1); + } + + std::vector prefix_sum(alphabet_size + 1); + for (auto symbol : data) { + prefix_sum[permutation_[symbol] + 1]++; + } + for (size_t i = 0; i < alphabet_size_; i++) { + prefix_sum[i + 1] += prefix_sum[i]; + } + + root_ = build_node( + 0, alphabet_size_, npos, + [&](node_index_t node) { return nodes_structure[node]; }, prefix_sum, + nodes); + for (auto symbol : data) { + auto index = permutation_[symbol]; + for (node_index_t current = root_; current != npos;) { + auto& node = nodes[current]; + bool go_right = index >= node.middle; + node.stream << go_right; + if (go_right) { + current = node.right_child; + } else { + current = node.left_child; + } + } + } + nodes_.reserve(nodes.size()); + for (auto& node : nodes) { + nodes_.emplace_back(std::move(node)); + } + } + + /** + * @brief Rank of specified symbol up to position pos (exclusive) + * + * @param symbol The character that the query is about + * @param pos Character index in [0, size()] + * @return Number of specified symbols in [0, pos) + * + */ + size_t rank_impl(uint64_t symbol, size_t pos) const { + if (symbol >= alphabet_size_) [[unlikely]] { + return 0; + } + symbol = permutation_[symbol]; + for (node_index_t current = root_; current != npos;) { + const WaveletNode& node = nodes_[current]; + if (symbol < node.middle) { + pos = node.data.rank0(pos); + current = node.left_child; + } else { + pos = node.data.rank(pos); + current = node.right_child; + } + } + return pos; + } + + /** + * @brief Select the position of the rank-th specified symbol (1-indexed) + * + * @param symbol The character that the query is about + * @param rank 1-indexed rank of specified symbol to select + * @return Symbol index, or size() if rank is out of range + * + */ + size_t select_impl(uint64_t symbol, size_t rank) const { + if (symbol >= alphabet_size_ || data_size_ == 0) [[unlikely]] { + return data_size_; + } + symbol = permutation_[symbol]; + node_index_t current = leaves_[symbol]; + for (; current != npos; current = nodes_[current].parent) { + const WaveletNode& node = nodes_[current]; + if (symbol < node.middle) { + rank = node.data.select0(rank) + 1; + } else { + rank = node.data.select(rank) + 1; + } + } + return rank - 1; + } + + /** + * @brief Accumulates the original data segment + * + * @param begin Begin of the segment + * @param end End of the segment + * @return Queried segment of data + * + */ + std::vector get_segment_impl(size_t begin, size_t end) const { + if (alphabet_size_ == 0 || data_size_ == 0 || begin >= end) [[unlikely]] { + return {}; + } + auto length = static_cast(end - begin); + std::vector result(2 * length); + copy_segment_content(root_, begin, end, + std::span{result.begin(), result.begin() + length}, + std::span{result.begin() + length, result.end()}); + result.resize(length); + return result; + } + + /** + * @return Returns the number of characters in data + * + */ + size_t size_impl() const { return data_size_; } + + /** + * @brief Writes a wavelet tree serialization to the bit stream + * + */ + void serialize(pixie::OutputBitStream& bs) const { + bs << alphabet_size_ << data_size_ << root_ << nodes_.size(); + for (const WaveletNode& node : nodes_) { + node.serialize(bs); + } + for (const node_index_t leaf : leaves_) { + bs << leaf; + } + for (const size_t idx : permutation_) { + bs << idx; + } + } + + static WaveletTreeIndex deserialize( + std::span& data) { + WaveletTreeIndex result; + auto read = [&data](auto& value) { + constexpr size_t length = sizeof(value); + std::memcpy(&value, data.data(), length); + data = data.subspan(length); + }; + read(result.alphabet_size_); + read(result.data_size_); + read(result.root_); + size_t size; + read(size); + result.nodes_.resize(size); + for (auto& node : result.nodes_) { + node = WaveletNode::deserialize(data); + } + result.leaves_.resize(result.alphabet_size_); + for (node_index_t& leaf : result.leaves_) { + read(leaf); + } + result.permutation_.resize(result.alphabet_size_); + for (size_t& index : result.permutation_) { + read(index); + } + result.inverse_permutation_.resize(result.alphabet_size_); + for (size_t i = 0; i < result.alphabet_size_; i++) { + result.inverse_permutation_[result.permutation_[i]] = i; + } + return result; + } +}; + +using WaveletTree = WaveletTreeIndex; +using WaveletTreeView = WaveletTreeIndex; + +} // namespace pixie diff --git a/include/reference_implementations/naive_rmm_tree.h b/include/references/naive_rmm_tree.h similarity index 100% rename from include/reference_implementations/naive_rmm_tree.h rename to include/references/naive_rmm_tree.h diff --git a/scripts/coverage_report.sh b/scripts/coverage_report.sh index de29220..5f19cda 100755 --- a/scripts/coverage_report.sh +++ b/scripts/coverage_report.sh @@ -11,14 +11,7 @@ find "${BUILD_DIR}" -name "*.gcda" -delete find "${BUILD_DIR}" -name "*.gcov" -delete rm -f "${BUILD_DIR}/coverage.txt" "${BUILD_DIR}/gcov_files.txt" -"${BUILD_DIR}/unittests" -"${BUILD_DIR}/excess_positions_tests" -"${BUILD_DIR}/louds_tree_tests" -"${BUILD_DIR}/bp_tree_tests" -"${BUILD_DIR}/dfuds_tree_tests" -"${BUILD_DIR}/test_rmm" -"${BUILD_DIR}/wavelet_tree_tests" -"${BUILD_DIR}/rmq_tests" +ctest --preset coverage cd "${BUILD_DIR}" find . -name "*.gcda" > gcov_files.txt diff --git a/scripts/plot_rmm.py b/scripts/plot_rmm.py index b543d4b..5bf652d 100644 --- a/scripts/plot_rmm.py +++ b/scripts/plot_rmm.py @@ -1,7 +1,7 @@ """ Plot RmMTree benchmark results (optionally compared with sdsl-lite). -This script reads a JSON produced by `bench_rmm` and, optionally, a JSON +This script reads a JSON produced by `rmm_benchmarks` and, optionally, a JSON with benchmarks for sdsl-lite. For each operation, it draws a scatter plot of individual points and a trend line (optionally median-smoothed) for each implementation on the same figure. @@ -125,7 +125,7 @@ def main(): ap.add_argument( "json", metavar="JSON", - help="Path to the JSON file with RmMTree results (output of bench_rmm).", + help="Path to the JSON file with RmMTree results (output of rmm_benchmarks).", ) ap.add_argument( "--sdsl-json", diff --git a/scripts/plot_rmq_hybrid_random_queries.py b/scripts/plot_rmq_hybrid_random_queries.py new file mode 100644 index 0000000..45b69eb --- /dev/null +++ b/scripts/plot_rmq_hybrid_random_queries.py @@ -0,0 +1,323 @@ +#!/usr/bin/env python3 +"""Plot RMQ hybrid variant timings for fully random query rows. + +The RMQ query benchmark uses the second positional argument as `max_width`. +When `max_width == N`, query widths are sampled uniformly from `[1, N]` and +left endpoints are sampled uniformly among valid positions for that width. +This script filters a Google Benchmark JSON file to those fully random rows and +plots the hybrid variants against input size. +""" + +from __future__ import annotations + +import argparse +import json +import os +import re +import statistics +import tempfile +from pathlib import Path +from typing import Dict, Iterable, List, NamedTuple, Sequence, Tuple + +os.environ.setdefault( + "MPLCONFIGDIR", str(Path(tempfile.gettempdir()) / "pixie-matplotlib") +) + +import matplotlib + +matplotlib.use("Agg") +import matplotlib.pyplot as plt + + +DEFAULT_METHODS = ("rmq_hybrid_btree", "rmq_cartesian_hybrid_btree") +METHOD_LABELS = { + "rmq_hybrid_btree": "HybridBTree", + "rmq_cartesian_hybrid_btree": "CartesianHybridBTree", +} +AGGREGATE_SUFFIXES = ("_mean", "_median", "_stddev", "_cv") +TIME_UNIT_TO_NS = { + "ns": 1.0, + "us": 1_000.0, + "ms": 1_000_000.0, + "s": 1_000_000_000.0, +} +TABLE_SEPARATOR_RE = re.compile(r"^-+$") + + +class Point(NamedTuple): + size: int + time_ns: float + + +def _clean_name(name: str) -> str: + for suffix in AGGREGATE_SUFFIXES: + if name.endswith(suffix): + return name[: -len(suffix)] + return name + + +def _base_name(bench: dict) -> str: + return _clean_name(bench.get("run_name") or bench.get("name", "")).split( + "/", 1 + )[0] + + +def _positional_args_from_name(name: str) -> List[int]: + args: List[int] = [] + parts = _clean_name(name).split("/") + for part in parts[1:]: + if ":" in part: + continue + try: + args.append(int(part)) + except ValueError: + continue + return args + + +def _extract_size_width(bench: dict) -> Tuple[int, int]: + params = bench.get("params", {}) + size = params.get("N", params.get("n", params.get("size"))) + width = params.get("max_width", params.get("width")) + if size is not None and width is not None: + return int(size), int(width) + + if "N" in bench and "max_width" in bench: + return int(bench["N"]), int(bench["max_width"]) + + positional_args = bench.get("args", []) + if len(positional_args) >= 2: + return int(positional_args[0]), int(positional_args[1]) + + name_args = _positional_args_from_name( + bench.get("run_name") or bench.get("name", "") + ) + if len(name_args) >= 2: + return name_args[0], name_args[1] + return 0, 0 + + +def _time_ns(bench: dict) -> float: + time = float(bench.get("cpu_time", bench.get("real_time", 0.0))) + return time * TIME_UNIT_TO_NS.get(bench.get("time_unit", "ns"), 1.0) + + +def _is_selected_aggregate(bench: dict, aggregate: str) -> bool: + name = bench.get("name", "") + if name.endswith(("_stddev", "_cv")): + return False + if bench.get("run_type") != "aggregate": + return False + return bench.get("aggregate_name", "") == aggregate or name.endswith( + f"_{aggregate}" + ) + + +def _is_iteration_row(bench: dict) -> bool: + name = bench.get("name", "") + if any(name.endswith(suffix) for suffix in AGGREGATE_SUFFIXES): + return False + return bench.get("run_type") in (None, "iteration") + + +def _collect_points( + benchmarks: Iterable[dict], methods: Sequence[str], aggregate: str +) -> Dict[str, List[Point]]: + aggregate_rows: List[dict] = [] + iteration_rows: List[dict] = [] + method_set = set(methods) + for bench in benchmarks: + if _base_name(bench) not in method_set: + continue + if _is_selected_aggregate(bench, aggregate): + aggregate_rows.append(bench) + elif _is_iteration_row(bench): + iteration_rows.append(bench) + + rows = aggregate_rows if aggregate_rows else iteration_rows + grouped: Dict[str, Dict[int, List[float]]] = {} + for bench in rows: + size, width = _extract_size_width(bench) + time_ns = _time_ns(bench) + if size <= 0 or width != size or time_ns <= 0: + continue + method = _base_name(bench) + grouped.setdefault(method, {}).setdefault(size, []).append(time_ns) + + series: Dict[str, List[Point]] = {} + for method, by_size in grouped.items(): + label = METHOD_LABELS.get(method, method) + series[label] = [ + Point(size, statistics.median(times)) + for size, times in sorted(by_size.items()) + ] + return series + + +def _parse_int_cell(cell: str) -> int: + value = cell.strip() + if value.startswith("2^"): + return 1 << int(value[2:]) + return int(value) + + +def _parse_time_cell(cell: str) -> float: + value = cell.strip() + if value == "-": + return 0.0 + return float(value) + + +def _collect_points_from_rmq_header( + path: Path, methods: Sequence[str] +) -> Dict[str, List[Point]]: + selected = set(methods) + by_label: Dict[str, List[Point]] = { + "HybridBTree": [], + "CartesianHybridBTree": [], + } + for raw_line in path.read_text(encoding="utf-8").splitlines(): + line = raw_line.strip() + if not line.startswith("* |"): + continue + row = line[1:].strip() + cells = [cell.strip() for cell in row.strip("|").split("|")] + if len(cells) < 7: + continue + if cells[0] == "N" or TABLE_SEPARATOR_RE.match(cells[0]): + continue + try: + size = _parse_int_cell(cells[0]) + max_width = _parse_int_cell(cells[1]) + hybrid_time = _parse_time_cell(cells[4]) + cartesian_hybrid_time = _parse_time_cell(cells[6]) + except ValueError: + continue + if max_width != size: + continue + if "rmq_hybrid_btree" in selected and hybrid_time > 0.0: + by_label["HybridBTree"].append(Point(size, hybrid_time)) + if ( + "rmq_cartesian_hybrid_btree" in selected + and cartesian_hybrid_time > 0.0 + ): + by_label["CartesianHybridBTree"].append( + Point(size, cartesian_hybrid_time) + ) + + return { + label: sorted(points) + for label, points in by_label.items() + if points + } + + +def _plot(series: Dict[str, List[Point]], output: Path, title: str) -> None: + fig, ax = plt.subplots(figsize=(9, 5.5)) + for label, points in sorted(series.items()): + ax.plot( + [point.size for point in points], + [point.time_ns for point in points], + marker="o", + markersize=3, + linewidth=0.8, + label=label, + ) + + ax.set_xscale("log", base=2) + ax.set_xlabel("Input size N") + ax.set_ylabel("CPU time per query (ns)") + ax.set_title(title) + ax.grid(True, which="both", linestyle="--", linewidth=0.5, alpha=0.35) + ax.legend(fontsize=8) + fig.tight_layout() + output.parent.mkdir(parents=True, exist_ok=True) + fig.savefig(output, dpi=180) + plt.close(fig) + + +def _print_summary(series: Dict[str, List[Point]]) -> None: + for label, points in sorted(series.items()): + formatted = ", ".join( + f"2^{point.size.bit_length() - 1}={point.time_ns:.2f}ns" + if point.size > 0 and point.size & (point.size - 1) == 0 + else f"{point.size}={point.time_ns:.2f}ns" + for point in points + ) + print(f"{label}: {formatted}") + + +def main() -> int: + parser = argparse.ArgumentParser( + description=( + "Plot rmq_hybrid_btree and rmq_cartesian_hybrid_btree rows where " + "max_width == N, i.e. fully random RMQ query widths." + ) + ) + parser.add_argument( + "input", + type=Path, + help="Google Benchmark JSON file, or include/pixie/rmq.h with --from-rmq-header", + ) + parser.add_argument( + "-o", + "--output", + type=Path, + default=Path("graphs/rmq_hybrid_random_queries.png"), + help="Output image path", + ) + parser.add_argument( + "--title", + default="RMQ hybrid variants on fully random queries", + help="Plot title", + ) + parser.add_argument( + "--aggregate", + default="mean", + choices=["mean", "median"], + help="Aggregate row to use when repetitions are present", + ) + parser.add_argument( + "--method", + action="append", + choices=DEFAULT_METHODS, + help="Benchmark method to include; defaults to both hybrid variants", + ) + parser.add_argument( + "--from-rmq-header", + action="store_true", + help="Parse the query benchmark table embedded at the top of include/pixie/rmq.h", + ) + parser.add_argument( + "--min-size", + type=int, + default=0, + help="Drop points with N smaller than this value", + ) + args = parser.parse_args() + + methods = tuple(args.method) if args.method else DEFAULT_METHODS + if args.from_rmq_header: + series = _collect_points_from_rmq_header(args.input, methods) + else: + data = json.loads(args.input.read_text(encoding="utf-8")) + benchmarks = data.get("benchmarks", data if isinstance(data, list) else []) + series = _collect_points(benchmarks, methods, args.aggregate) + if args.min_size > 0: + series = { + label: [point for point in points if point.size >= args.min_size] + for label, points in series.items() + } + series = {label: points for label, points in series.items() if points} + if not series: + raise SystemExit( + "No fully random hybrid RMQ rows found. Expected benchmark names " + "like rmq_hybrid_btree/N/N and rmq_cartesian_hybrid_btree/N/N." + ) + _plot(series, args.output, args.title) + _print_summary(series) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/run_cache_miss_profiler.sh b/scripts/run_cache_miss_profiler.sh deleted file mode 100755 index f6b64b4..0000000 --- a/scripts/run_cache_miss_profiler.sh +++ /dev/null @@ -1,4 +0,0 @@ -perf record -e cache-misses -g -- build/release-with-deb/benchmarks \ - --benchmark_filter=RankNonInterleaved/n:1717 \ - --benchmark_report_aggregates_only=true \ - --benchmark_perf_counters=INSTRUCTIONS,CYCLES,CACHE-MISSES \ No newline at end of file diff --git a/src/benchmarks/alignment_comparison.cpp b/src/benchmarks/alignment_comparison_benchmarks.cpp similarity index 100% rename from src/benchmarks/alignment_comparison.cpp rename to src/benchmarks/alignment_comparison_benchmarks.cpp diff --git a/src/benchmarks/benchmarks.cpp b/src/benchmarks/benchmarks.cpp deleted file mode 100644 index 8e5a9ea..0000000 --- a/src/benchmarks/benchmarks.cpp +++ /dev/null @@ -1,397 +0,0 @@ -#include -#include -#include - -#ifdef PIXIE_THIRD_PARTY_BENCHMARKS -// TODO: change the pasta/bit_vector usage of std::aligned_alloc -#include -#endif -#include -#include -#include -#include - -/** - * In literature bitvector length is usually measured up to 2^35, for simplicity - * were measure up to 2^30 where the time is mainly dominated by the main memory - * accesses. - */ - -constexpr size_t kBenchmarkRandomCopies = 8; -constexpr double warmup_time = 0.5; - -#ifdef _WIN32 -#include -void platform_setup() { - // Pin to core 3 - SetThreadAffinityMask(GetCurrentThread(), DWORD_PTR(1) << 3); - SetPriorityClass(GetCurrentProcess(), HIGH_PRIORITY_CLASS); - SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_TIME_CRITICAL); - SetProcessPriorityBoost(GetCurrentProcess(), TRUE); -} -#else -#include -void platform_setup() { - cpu_set_t cpuset; - CPU_ZERO(&cpuset); - CPU_SET(3, &cpuset); - sched_setaffinity(0, sizeof(cpuset), &cpuset); -} -#endif - -static void PrepareRandomBits50pFill(std::span bits) { - auto randomly_filled_length = bits.size() / kBenchmarkRandomCopies; - std::mt19937_64 rng(42); - - for (int i = 0; i < randomly_filled_length; ++i) { - bits[i] = rng(); - } - for (int i = 1; i < kBenchmarkRandomCopies; ++i) { - std::copy_n(bits.begin(), randomly_filled_length, - bits.begin() + i * randomly_filled_length); - } - for (int i = kBenchmarkRandomCopies * randomly_filled_length; i < bits.size(); - ++i) { - bits[i] = rng(); - } -} - -static void PrepareRandomBits12p5Fill(std::span bits) { - auto randomly_filled_length = bits.size() / kBenchmarkRandomCopies; - std::mt19937_64 rng(42); - - for (int i = 0; i < randomly_filled_length; ++i) { - bits[i] = rng() & rng() & rng(); - } - for (int i = 1; i < kBenchmarkRandomCopies; ++i) { - std::copy_n(bits.begin(), randomly_filled_length, - bits.begin() + i * randomly_filled_length); - } - for (int i = kBenchmarkRandomCopies * randomly_filled_length; i < bits.size(); - ++i) { - bits[i] = rng() & rng() & rng(); - } -} - -static void PrepareRandomBits87p5Fill(std::span bits) { - auto randomly_filled_length = bits.size() / kBenchmarkRandomCopies; - std::mt19937_64 rng(42); - - for (int i = 0; i < randomly_filled_length; ++i) { - bits[i] = rng() | rng() | rng(); - } - for (int i = 1; i < kBenchmarkRandomCopies; ++i) { - std::copy_n(bits.begin(), randomly_filled_length, - bits.begin() + i * randomly_filled_length); - } - for (int i = kBenchmarkRandomCopies * randomly_filled_length; i < bits.size(); - ++i) { - bits[i] = rng() | rng() | rng(); - } -} - -static void BM_RankNonInterleaved(benchmark::State& state) { - size_t n = state.range(0); - pixie::AlignedStorage bits(n); - auto bits_as_words = bits.As64BitInts(); - PrepareRandomBits50pFill(bits_as_words); - pixie::BitVector bv(bits_as_words, n); - std::mt19937_64 rng(42); - for (auto _ : state) { - uint64_t pos = rng() % n; - benchmark::DoNotOptimize(bv.rank(pos)); - } -} - -static void BM_RankZeroNonInterleaved(benchmark::State& state) { - size_t n = state.range(0); - - pixie::AlignedStorage bits(n); - PrepareRandomBits50pFill(bits.As64BitInts()); - pixie::BitVector bv(bits.As64BitInts(), n); - - std::mt19937_64 rng(42); - for (auto _ : state) { - uint64_t pos = rng() % n; - benchmark::DoNotOptimize(bv.rank0(pos)); - } -} - -static void BM_RankInterleaved(benchmark::State& state) { - size_t n = state.range(0); - - pixie::AlignedStorage bits(n); - PrepareRandomBits50pFill(bits.As64BitInts()); - pixie::BitVectorInterleaved bv(bits.As64BitInts(), n); - - std::mt19937_64 rng(42); - for (auto _ : state) { - uint64_t pos = rng() % n; - benchmark::DoNotOptimize(bv.rank(pos)); - } -} - -static void BM_SelectNonInterleaved(benchmark::State& state) { - size_t n = state.range(0); - - pixie::AlignedStorage bits(n); - PrepareRandomBits50pFill(bits.As64BitInts()); - pixie::BitVector bv(bits.As64BitInts(), n); - - auto max_rank = bv.rank(bv.size()) + 1; - - std::mt19937_64 rng(42); - for (auto _ : state) { - uint64_t rank = rng() % max_rank; - benchmark::DoNotOptimize(bv.select(rank)); - } -} - -static void BM_SelectZeroNonInterleaved(benchmark::State& state) { - size_t n = state.range(0); - - pixie::AlignedStorage bits(n); - PrepareRandomBits50pFill(bits.As64BitInts()); - pixie::BitVector bv(bits.As64BitInts(), n); - - auto max_rank0 = bv.rank0(bv.size()) + 1; - - std::mt19937_64 rng(42); - for (auto _ : state) { - uint64_t rank0 = rng() % max_rank0; - benchmark::DoNotOptimize(bv.select0(rank0)); - } -} - -static void BM_RankNonInterleaved12p5PercentFill(benchmark::State& state) { - size_t n = state.range(0); - - pixie::AlignedStorage bits(n); - PrepareRandomBits12p5Fill(bits.As64BitInts()); - pixie::BitVector bv(bits.As64BitInts(), n); - - std::mt19937_64 rng(42); - if (std::popcount(n) == 1) { - for (auto _ : state) { - uint64_t pos = rng() & (n - 1); - benchmark::DoNotOptimize(bv.rank(pos)); - } - } else { - for (auto _ : state) { - uint64_t pos = rng() % n; - benchmark::DoNotOptimize(bv.rank(pos)); - } - } -} - -static void BM_RankZeroNonInterleaved12p5PercentFill(benchmark::State& state) { - size_t n = state.range(0); - - pixie::AlignedStorage bits(n); - PrepareRandomBits12p5Fill(bits.As64BitInts()); - pixie::BitVector bv(bits.As64BitInts(), n); - - std::mt19937_64 rng(42); - for (auto _ : state) { - uint64_t pos = rng() % n; - benchmark::DoNotOptimize(bv.rank0(pos)); - } -} - -static void BM_SelectNonInterleaved12p5PercentFill(benchmark::State& state) { - size_t n = state.range(0); - - pixie::AlignedStorage bits(n); - PrepareRandomBits12p5Fill(bits.As64BitInts()); - pixie::BitVector bv(bits.As64BitInts(), n); - - auto max_rank = bv.rank(bv.size()) + 1; - - std::mt19937_64 rng(42); - for (auto _ : state) { - uint64_t rank = rng() % max_rank; - benchmark::DoNotOptimize(bv.select(rank)); - } -} - -static void BM_SelectZeroNonInterleaved12p5PercentFill( - benchmark::State& state) { - size_t n = state.range(0); - - pixie::AlignedStorage bits(n); - PrepareRandomBits12p5Fill(bits.As64BitInts()); - pixie::BitVector bv(bits.As64BitInts(), n); - - auto max_rank0 = bv.rank0(bv.size()) + 1; - - std::mt19937_64 rng(42); - for (auto _ : state) { - uint64_t rank0 = rng() % max_rank0; - benchmark::DoNotOptimize(bv.select0(rank0)); - } -} - -static void BM_RankNonInterleaved87p5PercentFill(benchmark::State& state) { - size_t n = state.range(0); - - pixie::AlignedStorage bits(n); - PrepareRandomBits87p5Fill(bits.As64BitInts()); - pixie::BitVector bv(bits.As64BitInts(), n); - - std::mt19937_64 rng(42); - for (auto _ : state) { - uint64_t pos = rng() % n; - benchmark::DoNotOptimize(bv.rank(pos)); - } -} - -static void BM_RankZeroNonInterleaved87p5PercentFill(benchmark::State& state) { - size_t n = state.range(0); - - pixie::AlignedStorage bits(n); - PrepareRandomBits87p5Fill(bits.As64BitInts()); - pixie::BitVector bv(bits.As64BitInts(), n); - - std::mt19937_64 rng(42); - for (auto _ : state) { - uint64_t pos = rng() % n; - benchmark::DoNotOptimize(bv.rank0(pos)); - } -} - -static void BM_SelectNonInterleaved87p5PercentFill(benchmark::State& state) { - size_t n = state.range(0); - - pixie::AlignedStorage bits(n); - PrepareRandomBits87p5Fill(bits.As64BitInts()); - pixie::BitVector bv(bits.As64BitInts(), n); - - auto max_rank = bv.rank(bv.size()) + 1; - - std::mt19937_64 rng(42); - for (auto _ : state) { - uint64_t rank = rng() % max_rank; - benchmark::DoNotOptimize(bv.select(rank)); - } -} - -static void BM_SelectZeroNonInterleaved87p5PercentFill( - benchmark::State& state) { - size_t n = state.range(0); - - pixie::AlignedStorage bits(n); - PrepareRandomBits87p5Fill(bits.As64BitInts()); - pixie::BitVector bv(bits.As64BitInts(), n); - - auto max_rank0 = bv.rank0(bv.size()) + 1; - - std::mt19937_64 rng(42); - for (auto _ : state) { - uint64_t rank0 = rng() % max_rank0; - benchmark::DoNotOptimize(bv.select(rank0)); - } -} - -BENCHMARK(BM_RankInterleaved) - ->ArgNames({"n"}) - ->RangeMultiplier(4) - ->Range(1ull << 10, 1ull << 30) - ->Iterations(10000000) - ->MinWarmUpTime(warmup_time) - ->Repetitions(10); - -BENCHMARK(BM_RankNonInterleaved) - ->ArgNames({"n"}) - ->RangeMultiplier(4) - ->Range(1ull << 10, 1ull << 30) - ->Iterations(10000000) - ->MinWarmUpTime(warmup_time) - ->Repetitions(10); - -BENCHMARK(BM_RankZeroNonInterleaved) - ->ArgNames({"n"}) - ->RangeMultiplier(4) - ->Range(1ull << 10, 1ull << 30) - ->Iterations(10000000) - ->MinWarmUpTime(warmup_time) - ->Repetitions(10); - -BENCHMARK(BM_SelectNonInterleaved) - ->ArgNames({"n"}) - ->RangeMultiplier(4) - ->Range(1ull << 10, 1ull << 30) - ->Iterations(5000000) - ->MinWarmUpTime(warmup_time) - ->Repetitions(10); - -BENCHMARK(BM_SelectZeroNonInterleaved) - ->ArgNames({"n"}) - ->RangeMultiplier(4) - ->Range(1ull << 10, 1ull << 30) - ->Iterations(5000000) - ->MinWarmUpTime(warmup_time) - ->Repetitions(10); - -BENCHMARK(BM_RankNonInterleaved12p5PercentFill) - ->ArgNames({"n"}) - ->RangeMultiplier(4) - ->Range(1ull << 10, 1ull << 30) - ->Iterations(10000000) - ->MinWarmUpTime(warmup_time) - ->Repetitions(10); - -BENCHMARK(BM_RankZeroNonInterleaved12p5PercentFill) - ->ArgNames({"n"}) - ->RangeMultiplier(4) - ->Range(1ull << 10, 1ull << 30) - ->Iterations(10000000) - ->MinWarmUpTime(warmup_time) - ->Repetitions(10); - -BENCHMARK(BM_SelectNonInterleaved12p5PercentFill) - ->ArgNames({"n"}) - ->RangeMultiplier(4) - ->Range(1ull << 10, 1ull << 30) - ->Iterations(5000000) - ->MinWarmUpTime(warmup_time) - ->Repetitions(10); - -BENCHMARK(BM_SelectZeroNonInterleaved12p5PercentFill) - ->ArgNames({"n"}) - ->RangeMultiplier(4) - ->Range(1ull << 10, 1ull << 30) - ->Iterations(5000000) - ->MinWarmUpTime(warmup_time) - ->Repetitions(10); - -BENCHMARK(BM_RankNonInterleaved87p5PercentFill) - ->ArgNames({"n"}) - ->RangeMultiplier(4) - ->Range(1ull << 10, 1ull << 30) - ->Iterations(10000000) - ->MinWarmUpTime(warmup_time) - ->Repetitions(10); - -BENCHMARK(BM_RankZeroNonInterleaved87p5PercentFill) - ->ArgNames({"n"}) - ->RangeMultiplier(4) - ->Range(1ull << 10, 1ull << 30) - ->Iterations(10000000) - ->MinWarmUpTime(warmup_time) - ->Repetitions(10); - -BENCHMARK(BM_SelectNonInterleaved87p5PercentFill) - ->ArgNames({"n"}) - ->RangeMultiplier(4) - ->Range(1ull << 10, 1ull << 30) - ->Iterations(5000000) - ->MinWarmUpTime(warmup_time) - ->Repetitions(10); - -BENCHMARK(BM_SelectZeroNonInterleaved87p5PercentFill) - ->ArgNames({"n"}) - ->RangeMultiplier(4) - ->Range(1ull << 10, 1ull << 30) - ->Iterations(5000000) - ->MinWarmUpTime(warmup_time) - ->Repetitions(10); diff --git a/src/benchmarks/bp_tree_benchmarks.cpp b/src/benchmarks/bp_tree_benchmarks.cpp index d390a7e..2f48277 100644 --- a/src/benchmarks/bp_tree_benchmarks.cpp +++ b/src/benchmarks/bp_tree_benchmarks.cpp @@ -1,12 +1,12 @@ #include -#include +#include #include #include #ifdef SDSL_SUPPORT #pragma message("SDSL_SUPPORT enabled") -#include "pixie/rmm_tree_sdsl.h" +#include using BPTree = pixie::BPTree; #else #pragma message("SDSL_SUPPORT disabled") diff --git a/src/benchmarks/dfuds_tree_benchmarks.cpp b/src/benchmarks/dfuds_tree_benchmarks.cpp index 7eff6fa..d0dfd6d 100644 --- a/src/benchmarks/dfuds_tree_benchmarks.cpp +++ b/src/benchmarks/dfuds_tree_benchmarks.cpp @@ -1,12 +1,12 @@ #include -#include +#include #include #include #ifdef SDSL_SUPPORT #pragma message("SDSL_SUPPORT enabled") -#include "pixie/rmm_tree_sdsl.h" +#include using DFUDSTree = pixie::DFUDSTree; #else #pragma message("SDSL_SUPPORT disabled") diff --git a/src/benchmarks/louds_tree_benchmarks.cpp b/src/benchmarks/louds_tree_benchmarks.cpp index 9b47ec1..6c43a52 100644 --- a/src/benchmarks/louds_tree_benchmarks.cpp +++ b/src/benchmarks/louds_tree_benchmarks.cpp @@ -1,5 +1,5 @@ #include -#include +#include #include #include diff --git a/src/benchmarks/rank_select_benchmarks.cpp b/src/benchmarks/rank_select_benchmarks.cpp new file mode 100644 index 0000000..122f893 --- /dev/null +++ b/src/benchmarks/rank_select_benchmarks.cpp @@ -0,0 +1,499 @@ +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +constexpr std::uint64_t kSeed = 42; +constexpr std::size_t kQueryPoolBytes = 512 * 1024; +constexpr std::size_t kQueryCount = kQueryPoolBytes / sizeof(std::size_t); +constexpr double kBenchmarkWarmupSeconds = 0.2; +constexpr double kBenchmarkMinSeconds = 1.0; +constexpr std::array kSizes = { + 1ull << 10, 1ull << 14, 1ull << 18, 1ull << 22, + 1ull << 26, 1ull << 30, 1ull << 34}; +// FNBP construction is linear in encoded bits. Include a 2^30-bit case to +// expose large-source behavior while retaining the existing scaling points. +constexpr std::array kFnbpSizes = { + 1ull << 10, 1ull << 14, 1ull << 18, 1ull << 22, 1ull << 26, 1ull << 30}; +using RankSelect = pixie::RankSelectSupport<>; +constexpr RankSelect::SelectSupport kMeasuredSelectSupport = + RankSelect::SelectSupport::kBoth; + +static_assert(kQueryCount > 0 && (kQueryCount & (kQueryCount - 1)) == 0); + +enum class Fill { + k12p5, + k50, + k87p5, +}; + +enum class QueryOperation { + kRank1, + kRank0, + kSelect1, + kSelect0, +}; + +struct FillSpec { + std::string_view name; + double expected_one_fill_percent; + std::uint64_t seed_tag; +}; + +constexpr FillSpec fill_spec(Fill fill) { + switch (fill) { + case Fill::k12p5: + return {"12p5", 12.5, 0xC6A4A7935BD1E995ull}; + case Fill::k50: + return {"50", 50.0, 0x9E3779B97F4A7C15ull}; + case Fill::k87p5: + return {"87p5", 87.5, 0xD6E8FEB86659FD93ull}; + } + return {"unknown", 0.0, 0}; +} + +std::uint64_t splitmix64(std::uint64_t value) { + value += 0x9E3779B97F4A7C15ull; + value = (value ^ (value >> 30)) * 0xBF58476D1CE4E5B9ull; + value = (value ^ (value >> 27)) * 0x94D049BB133111EBull; + return value ^ (value >> 31); +} + +std::uint64_t mix_seed(std::uint64_t seed, std::uint64_t value) { + return splitmix64(seed ^ splitmix64(value)); +} + +std::uint64_t fnbp_priority(std::uint64_t seed, std::size_t value_index) { + return splitmix64(seed + static_cast(value_index)); +} + +void fill_fnbp_words(std::span words, + std::size_t bit_count, + std::uint64_t seed) { + std::fill(words.begin(), words.end(), 0); + + // This is the same backward monotone-stack construction used for the + // Ferrada-Navarro BP encoding in CartesianHybridBTree. + const std::size_t value_count = bit_count / 2; + pixie::rmq::utils::SuccinctIncreasingStack monotone_stack(value_count); + std::size_t write_position = bit_count; + + const auto prepend_open = [&] { + --write_position; + words[write_position >> 6] |= std::uint64_t{1} << (write_position & 63); + }; + + for (std::size_t value_index = value_count; value_index > 0; --value_index) { + const std::size_t current_index = value_index - 1; + const std::uint64_t value = fnbp_priority(seed, current_index); + while (!monotone_stack.empty() && + fnbp_priority(seed, value_count - 1 - monotone_stack.top()) >= + value) { + monotone_stack.pop(); + prepend_open(); + } + monotone_stack.push(value_count - 1 - current_index); + --write_position; // Closing parenthesis. + } + + while (write_position != 0) { + prepend_open(); + } +} + +class BenchmarkRepetitionSeeds { + public: + std::uint64_t repetition_index(const benchmark::State& state, + std::size_t size, + Fill fill) { + std::lock_guard lock(mutex_); + Entry& entry = entries_[key_for(state, size, fill)]; + const benchmark::IterationCount max_iterations = state.max_iterations; + if (!entry.seen) { + entry.seen = true; + entry.last_max_iterations = max_iterations; + return entry.repetition_index; + } + + if (max_iterations == entry.last_max_iterations) { + if (!entry.saw_iteration_count_change && + !entry.skipped_initial_warmup_equal) { + entry.skipped_initial_warmup_equal = true; + } else { + ++entry.repetition_index; + } + } else { + entry.saw_iteration_count_change = true; + } + entry.last_max_iterations = max_iterations; + return entry.repetition_index; + } + + private: + struct Entry { + benchmark::IterationCount last_max_iterations = 0; + std::uint64_t repetition_index = 0; + bool seen = false; + bool saw_iteration_count_change = false; + bool skipped_initial_warmup_equal = kBenchmarkWarmupSeconds == 0.0; + }; + + std::string key_for(const benchmark::State& state, + std::size_t size, + Fill fill) const { + return state.name() + "/" + std::to_string(size) + "/" + + std::string(fill_spec(fill).name); + } + + std::mutex mutex_; + std::unordered_map entries_; +}; + +BenchmarkRepetitionSeeds& repetition_seeds() { + static BenchmarkRepetitionSeeds seeds; + return seeds; +} + +struct SeedContext { + std::uint64_t repetition_index = 0; + std::uint64_t source_seed = 0; + std::uint64_t query_seed = 0; +}; + +SeedContext make_seed_context(const benchmark::State& state, + std::size_t size, + Fill fill) { + const std::uint64_t repetition_index = + repetition_seeds().repetition_index(state, size, fill); + std::uint64_t seed = mix_seed(kSeed, static_cast(size)); + seed = mix_seed(seed, fill_spec(fill).seed_tag); + seed = mix_seed(seed, repetition_index); + + return { + .repetition_index = repetition_index, + .source_seed = mix_seed(seed, 0x4F1BBCDCBFA54005ull), + .query_seed = mix_seed(seed, 0x9D2C5680D3F6C58Bull), + }; +} + +void fill_words(std::span words, Fill fill, std::uint64_t seed) { + std::mt19937_64 rng(seed); + for (std::uint64_t& word : words) { + switch (fill) { + case Fill::k12p5: + word = rng() & rng() & rng(); + break; + case Fill::k50: + word = rng(); + break; + case Fill::k87p5: + word = rng() | rng() | rng(); + break; + } + } +} + +class BitDataset { + public: + BitDataset(std::size_t size, Fill fill, std::uint64_t seed) + : source_(size), size_(size) { + fill_words(source_.writable_words64(), fill, seed); + } + + const pixie::AlignedStorage& source() const { return source_; } + std::size_t size() const { return size_; } + + private: + pixie::AlignedStorage source_; + std::size_t size_ = 0; +}; + +class FnbpDataset { + public: + FnbpDataset(std::size_t size, std::uint64_t seed) + : source_(size), size_(size) { + fill_fnbp_words(source_.writable_words64(), size_, seed); + } + + const pixie::AlignedStorage& source() const { return source_; } + std::size_t size() const { return size_; } + + private: + pixie::AlignedStorage source_; + std::size_t size_ = 0; +}; + +std::vector make_query_pool(std::size_t first, + std::size_t last, + std::uint64_t seed) { + std::mt19937_64 rng(seed); + std::uniform_int_distribution distribution(first, last); + std::vector queries(kQueryCount); + for (std::size_t& query : queries) { + query = distribution(rng); + } + return queries; +} + +void set_common_counters(benchmark::State& state, + std::size_t size, + Fill fill, + std::size_t auxiliary_bytes, + std::uint64_t repetition_index) { + const double input_bytes = static_cast((size + 7) / 8); + const double auxiliary_bytes_as_double = static_cast(auxiliary_bytes); + state.counters["N"] = static_cast(size); + state.counters["one_fill_percent"] = + fill_spec(fill).expected_one_fill_percent; + state.counters["input_bytes"] = input_bytes; + state.counters["aux_bytes"] = auxiliary_bytes_as_double; + state.counters["aux_mib"] = auxiliary_bytes_as_double / (1024.0 * 1024.0); + state.counters["aux_bits_per_input_bit"] = + size == 0 ? 0.0 : 8.0 * auxiliary_bytes_as_double / size; + state.counters["select1_enabled"] = 1.0; + state.counters["select0_enabled"] = 1.0; + state.counters["seed_repetition"] = static_cast(repetition_index); +} + +void set_fnbp_counters(benchmark::State& state, + std::size_t size, + std::size_t auxiliary_bytes, + std::uint64_t repetition_index) { + set_common_counters(state, size, Fill::k50, auxiliary_bytes, + repetition_index); + state.counters["fnbp_source"] = 1.0; +} + +template +void run_build(benchmark::State& state) { + const std::size_t size = static_cast(state.range(0)); + const SeedContext seeds = make_seed_context(state, size, fill); + const BitDataset dataset(size, fill, seeds.source_seed); + std::size_t auxiliary_bytes = 0; + + for (auto _ : state) { + RankSelect support(dataset.source(), dataset.size(), + kMeasuredSelectSupport); + auxiliary_bytes = support.memory_usage_bytes(); + benchmark::DoNotOptimize(auxiliary_bytes); + benchmark::ClobberMemory(); + } + + set_common_counters(state, size, fill, auxiliary_bytes, + seeds.repetition_index); + state.SetItemsProcessed(static_cast(state.iterations()) * + static_cast(size)); +} + +template +void run_query(benchmark::State& state) { + const std::size_t size = static_cast(state.range(0)); + const SeedContext seeds = make_seed_context(state, size, fill); + const BitDataset dataset(size, fill, seeds.source_seed); + const RankSelect support(dataset.source(), dataset.size(), + kMeasuredSelectSupport); + const std::size_t one_count = support.rank(support.size()); + const std::size_t zero_count = support.rank0(support.size()); + + std::vector queries; + if constexpr (operation == QueryOperation::kRank1 || + operation == QueryOperation::kRank0) { + queries = make_query_pool(0, size, seeds.query_seed); + } else if constexpr (operation == QueryOperation::kSelect1) { + if (one_count == 0) { + state.SkipWithError("input has no one bits"); + return; + } + queries = make_query_pool(1, one_count, seeds.query_seed); + } else { + if (zero_count == 0) { + state.SkipWithError("input has no zero bits"); + return; + } + queries = make_query_pool(1, zero_count, seeds.query_seed); + } + + std::size_t query_index = 0; + for (auto _ : state) { + const std::size_t query = queries[query_index++ & (kQueryCount - 1)]; + if constexpr (operation == QueryOperation::kRank1) { + benchmark::DoNotOptimize(support.rank(query)); + } else if constexpr (operation == QueryOperation::kRank0) { + benchmark::DoNotOptimize(support.rank0(query)); + } else if constexpr (operation == QueryOperation::kSelect1) { + benchmark::DoNotOptimize(support.select(query)); + } else { + benchmark::DoNotOptimize(support.select0(query)); + } + } + + set_common_counters(state, size, fill, support.memory_usage_bytes(), + seeds.repetition_index); + state.SetItemsProcessed(static_cast(state.iterations())); +} + +void run_fnbp_build(benchmark::State& state) { + const std::size_t size = static_cast(state.range(0)); + const SeedContext seeds = make_seed_context(state, size, Fill::k50); + const FnbpDataset dataset(size, seeds.source_seed); + std::size_t auxiliary_bytes = 0; + + for (auto _ : state) { + RankSelect support(dataset.source(), dataset.size(), + kMeasuredSelectSupport); + auxiliary_bytes = support.memory_usage_bytes(); + benchmark::DoNotOptimize(auxiliary_bytes); + benchmark::ClobberMemory(); + } + + set_fnbp_counters(state, size, auxiliary_bytes, seeds.repetition_index); + state.SetItemsProcessed(static_cast(state.iterations()) * + static_cast(size)); +} + +template +void run_fnbp_query(benchmark::State& state) { + const std::size_t size = static_cast(state.range(0)); + const SeedContext seeds = make_seed_context(state, size, Fill::k50); + const FnbpDataset dataset(size, seeds.source_seed); + const RankSelect support(dataset.source(), dataset.size(), + kMeasuredSelectSupport); + const std::size_t one_count = support.rank(support.size()); + const std::size_t zero_count = support.rank0(support.size()); + if (one_count != size / 2 || zero_count != size / 2) { + state.SkipWithError("FNBP source is not balanced"); + return; + } + + std::vector queries; + if constexpr (operation == QueryOperation::kRank1 || + operation == QueryOperation::kRank0) { + queries = make_query_pool(0, size, seeds.query_seed); + } else if constexpr (operation == QueryOperation::kSelect1) { + queries = make_query_pool(1, one_count, seeds.query_seed); + } else { + queries = make_query_pool(1, zero_count, seeds.query_seed); + } + + std::size_t query_index = 0; + for (auto _ : state) { + const std::size_t query = queries[query_index++ & (kQueryCount - 1)]; + if constexpr (operation == QueryOperation::kRank1) { + benchmark::DoNotOptimize(support.rank(query)); + } else if constexpr (operation == QueryOperation::kRank0) { + benchmark::DoNotOptimize(support.rank0(query)); + } else if constexpr (operation == QueryOperation::kSelect1) { + benchmark::DoNotOptimize(support.select(query)); + } else { + benchmark::DoNotOptimize(support.select0(query)); + } + } + + set_fnbp_counters(state, size, support.memory_usage_bytes(), + seeds.repetition_index); + state.SetItemsProcessed(static_cast(state.iterations())); +} + +template +void register_build_row() { + const std::string name = + "rank_select_build_both_" + std::string(fill_spec(fill).name); + auto* row = benchmark::RegisterBenchmark(name.c_str(), &run_build); + for (const std::size_t size : kSizes) { + row->Arg(static_cast(size)); + } + row->ArgNames({"N"}) + ->Unit(benchmark::kMillisecond) + ->MinWarmUpTime(kBenchmarkWarmupSeconds) + ->MinTime(kBenchmarkMinSeconds); +} + +template +void register_query_row(std::string_view operation_name) { + const std::string name = "rank_select_" + std::string(operation_name) + "_" + + std::string(fill_spec(fill).name); + auto* row = + benchmark::RegisterBenchmark(name.c_str(), &run_query); + for (const std::size_t size : kSizes) { + row->Arg(static_cast(size)); + } + row->ArgNames({"N"}) + ->Unit(benchmark::kNanosecond) + ->MinWarmUpTime(kBenchmarkWarmupSeconds) + ->MinTime(kBenchmarkMinSeconds); +} + +template +void register_fill_rows() { + register_build_row(); + register_query_row("rank1"); + register_query_row("rank0"); + register_query_row("select1"); + register_query_row("select0"); +} + +void register_fnbp_build_row() { + auto* row = benchmark::RegisterBenchmark("rank_select_fnbp_build_both", + &run_fnbp_build); + for (const std::size_t size : kFnbpSizes) { + row->Arg(static_cast(size)); + } + row->ArgNames({"N"}) + ->Unit(benchmark::kMillisecond) + ->MinWarmUpTime(kBenchmarkWarmupSeconds) + ->MinTime(kBenchmarkMinSeconds); +} + +template +void register_fnbp_query_row(std::string_view operation_name) { + const std::string name = "rank_select_fnbp_" + std::string(operation_name); + auto* row = + benchmark::RegisterBenchmark(name.c_str(), &run_fnbp_query); + for (const std::size_t size : kFnbpSizes) { + row->Arg(static_cast(size)); + } + row->ArgNames({"N"}) + ->Unit(benchmark::kNanosecond) + ->MinWarmUpTime(kBenchmarkWarmupSeconds) + ->MinTime(kBenchmarkMinSeconds); +} + +void register_fnbp_rows() { + register_fnbp_build_row(); + register_fnbp_query_row("rank1"); + register_fnbp_query_row("rank0"); + register_fnbp_query_row("select1"); + register_fnbp_query_row("select0"); +} + +void register_benchmarks() { + register_fill_rows(); + register_fill_rows(); + register_fill_rows(); + register_fnbp_rows(); +} + +} // namespace + +int main(int argc, char** argv) { + benchmark::MaybeReenterWithoutASLR(argc, argv); + benchmark::Initialize(&argc, argv); + register_benchmarks(); + benchmark::RunSpecifiedBenchmarks(); + benchmark::Shutdown(); + return 0; +} diff --git a/src/benchmarks/bench_rmm.cpp b/src/benchmarks/rmm_benchmarks.cpp similarity index 80% rename from src/benchmarks/bench_rmm.cpp rename to src/benchmarks/rmm_benchmarks.cpp index 38e0102..26ab09a 100644 --- a/src/benchmarks/bench_rmm.cpp +++ b/src/benchmarks/rmm_benchmarks.cpp @@ -1,4 +1,4 @@ -#include +#include #include "rmm_benchmark_base.h" diff --git a/src/benchmarks/bench_rmm_btree.cpp b/src/benchmarks/rmm_btree_benchmarks.cpp similarity index 91% rename from src/benchmarks/bench_rmm_btree.cpp rename to src/benchmarks/rmm_btree_benchmarks.cpp index 1c18854..2056ef8 100644 --- a/src/benchmarks/bench_rmm_btree.cpp +++ b/src/benchmarks/rmm_btree_benchmarks.cpp @@ -1,4 +1,4 @@ -#include +#include #include "rmm_benchmark_base.h" diff --git a/src/benchmarks/bench_rmm_sdsl.cpp b/src/benchmarks/rmm_sdsl_benchmarks.cpp similarity index 94% rename from src/benchmarks/bench_rmm_sdsl.cpp rename to src/benchmarks/rmm_sdsl_benchmarks.cpp index e722200..6d632ba 100644 --- a/src/benchmarks/bench_rmm_sdsl.cpp +++ b/src/benchmarks/rmm_sdsl_benchmarks.cpp @@ -1,4 +1,4 @@ -#include +#include #include diff --git a/src/benchmarks/bench_rmq.cpp b/src/benchmarks/rmq_benchmarks.cpp similarity index 99% rename from src/benchmarks/bench_rmq.cpp rename to src/benchmarks/rmq_benchmarks.cpp index 7282069..fe0afae 100644 --- a/src/benchmarks/bench_rmq.cpp +++ b/src/benchmarks/rmq_benchmarks.cpp @@ -1,11 +1,5 @@ #include -#include -#include -#include - -#ifdef PIXIE_THIRD_PARTY_BENCHMARKS -#include -#endif +#include #include #include diff --git a/src/benchmarks/wavelet_tree_benchmarks.cpp b/src/benchmarks/wavelet_tree_benchmarks.cpp index 1a35142..1ac1249 100644 --- a/src/benchmarks/wavelet_tree_benchmarks.cpp +++ b/src/benchmarks/wavelet_tree_benchmarks.cpp @@ -1,6 +1,6 @@ #include #include -#include +#include #include @@ -64,7 +64,7 @@ static void BM_WaveletTreeRank(benchmark::State& state) { } } -static void BM_WaveletTreeMmapSelect(benchmark::State& state) { +static void BM_WaveletTreeViewSelect(benchmark::State& state) { size_t data_size = state.range(0), alphabet_size = 1024, query = data_size; std::mt19937_64 rng(239); @@ -94,18 +94,17 @@ static void BM_WaveletTreeMmapSelect(benchmark::State& state) { state.ResumeTiming(); - auto mmap_tree = - pixie::WaveletTreeBase::deserialize(byte_span); - benchmark::DoNotOptimize(mmap_tree); + auto view_tree = pixie::WaveletTreeView::deserialize(byte_span); + benchmark::DoNotOptimize(view_tree); for (size_t i = 0; i < query; i++) { - size_t select = mmap_tree.select(query_symbol[i], query_pos[i]); + size_t select = view_tree.select(query_symbol[i], query_pos[i]); benchmark::DoNotOptimize(select); } } } -static void BM_WaveletTreeMmapRank(benchmark::State& state) { +static void BM_WaveletTreeViewRank(benchmark::State& state) { size_t data_size = state.range(0), alphabet_size = 1024, query = data_size; std::mt19937_64 rng(239); @@ -129,18 +128,17 @@ static void BM_WaveletTreeMmapRank(benchmark::State& state) { state.ResumeTiming(); - auto mmap_tree = - pixie::WaveletTreeBase::deserialize(byte_span); - benchmark::DoNotOptimize(mmap_tree); + auto view_tree = pixie::WaveletTreeView::deserialize(byte_span); + benchmark::DoNotOptimize(view_tree); for (size_t i = 0; i < query; i++) { - size_t rank = mmap_tree.rank(query_symbol[i], query_pos[i]); + size_t rank = view_tree.rank(query_symbol[i], query_pos[i]); benchmark::DoNotOptimize(rank); } } } -static void BM_WaveletTreeMmapSegment(benchmark::State& state) { +static void BM_WaveletTreeViewSegment(benchmark::State& state) { size_t data_size = state.range(0), alphabet_size = 1024, query = data_size / 128, length = 128; std::mt19937_64 rng(239); @@ -163,12 +161,11 @@ static void BM_WaveletTreeMmapSegment(benchmark::State& state) { state.ResumeTiming(); - auto mmap_tree = - pixie::WaveletTreeBase::deserialize(byte_span); - benchmark::DoNotOptimize(mmap_tree); + auto view_tree = pixie::WaveletTreeView::deserialize(byte_span); + benchmark::DoNotOptimize(view_tree); for (size_t i = 0; i < query; i++) { - auto segment = mmap_tree.get_segment(query_pos[i], query_pos[i] + length); + auto segment = view_tree.get_segment(query_pos[i], query_pos[i] + length); benchmark::DoNotOptimize(segment); } } @@ -198,37 +195,37 @@ BENCHMARK(BM_WaveletTreeRank) ->Range(1ull << 18, 1ull << 26) ->Iterations(10); -BENCHMARK(BM_WaveletTreeMmapSelect) +BENCHMARK(BM_WaveletTreeViewSelect) ->ArgNames({"data_size"}) ->RangeMultiplier(2) ->Range(1ull << 8, 1ull << 18) ->Iterations(100); -BENCHMARK(BM_WaveletTreeMmapSelect) +BENCHMARK(BM_WaveletTreeViewSelect) ->ArgNames({"data_size"}) ->RangeMultiplier(2) ->Range(1ull << 18, 1ull << 26) ->Iterations(10); -BENCHMARK(BM_WaveletTreeMmapRank) +BENCHMARK(BM_WaveletTreeViewRank) ->ArgNames({"data_size"}) ->RangeMultiplier(2) ->Range(1ull << 8, 1ull << 18) ->Iterations(100); -BENCHMARK(BM_WaveletTreeMmapRank) +BENCHMARK(BM_WaveletTreeViewRank) ->ArgNames({"data_size"}) ->RangeMultiplier(2) ->Range(1ull << 18, 1ull << 26) ->Iterations(10); -BENCHMARK(BM_WaveletTreeMmapSegment) +BENCHMARK(BM_WaveletTreeViewSegment) ->ArgNames({"data_size"}) ->RangeMultiplier(2) ->Range(1ull << 8, 1ull << 18) ->Iterations(100); -BENCHMARK(BM_WaveletTreeMmapSegment) +BENCHMARK(BM_WaveletTreeViewSegment) ->ArgNames({"data_size"}) ->RangeMultiplier(2) ->Range(1ull << 18, 1ull << 26) diff --git a/src/tests/benchmark_tests.cpp b/src/tests/benchmark_tests.cpp index 6a24b93..5586754 100644 --- a/src/tests/benchmark_tests.cpp +++ b/src/tests/benchmark_tests.cpp @@ -1,14 +1,11 @@ #include #include -#include +#include #include #include -using pixie::BitVector; -using pixie::BitVectorInterleaved; - -TEST(BitVectorBenchmarkTest, SelectNonInterleaved10PercentFill) { +TEST(RankSelectBenchmarkTest, Select10PercentFill) { for (size_t n = 8; n <= (1ull << 28); n <<= 2) { std::mt19937_64 rng(42); @@ -19,7 +16,7 @@ TEST(BitVectorBenchmarkTest, SelectNonInterleaved10PercentFill) { bits[pos / 64] |= (1ULL << pos % 64); } - pixie::BitVector bv(bits, n); + pixie::RankSelectSupport<> bv(bits, n); auto max_rank = bv.rank(bv.size()) + 1; for (int i = 0; i < 100000; i++) { @@ -29,7 +26,7 @@ TEST(BitVectorBenchmarkTest, SelectNonInterleaved10PercentFill) { } } -TEST(BitVectorBenchmarkTest, SelectZeroNonInterleaved10PercentFill) { +TEST(RankSelectBenchmarkTest, SelectZero10PercentFill) { for (size_t n = 8; n <= (1ull << 28); n <<= 2) { std::mt19937_64 rng(42); @@ -40,7 +37,7 @@ TEST(BitVectorBenchmarkTest, SelectZeroNonInterleaved10PercentFill) { bits[pos / 64] |= (1ULL << pos % 64); } - pixie::BitVector bv(bits, n); + pixie::RankSelectSupport<> bv(bits, n); auto max_rank0 = bv.rank0(bv.size()) + 1; for (int i = 0; i < 100000; i++) { @@ -50,7 +47,7 @@ TEST(BitVectorBenchmarkTest, SelectZeroNonInterleaved10PercentFill) { } } -TEST(BitVectorBenchmarkTest, SelectNonInterleaved90PercentFill) { +TEST(RankSelectBenchmarkTest, Select90PercentFill) { for (size_t n = 8; n <= (1ull << 28); n <<= 2) { std::mt19937_64 rng(42); @@ -61,7 +58,7 @@ TEST(BitVectorBenchmarkTest, SelectNonInterleaved90PercentFill) { bits[pos / 64] |= (1ULL << pos % 64); } - pixie::BitVector bv(bits, n); + pixie::RankSelectSupport<> bv(bits, n); auto max_rank = bv.rank(bv.size()) + 1; for (int i = 0; i < 100000; i++) { @@ -71,7 +68,7 @@ TEST(BitVectorBenchmarkTest, SelectNonInterleaved90PercentFill) { } } -TEST(BitVectorBenchmarkTest, SelectZeroNonInterleaved90PercentFill) { +TEST(RankSelectBenchmarkTest, SelectZero90PercentFill) { for (size_t n = 8; n <= (1ull << 28); n <<= 2) { std::mt19937_64 rng(42); @@ -82,7 +79,7 @@ TEST(BitVectorBenchmarkTest, SelectZeroNonInterleaved90PercentFill) { bits[pos / 64] |= (1ULL << pos % 64); } - pixie::BitVector bv(bits, n); + pixie::RankSelectSupport<> bv(bits, n); auto max_rank0 = bv.rank0(bv.size()) + 1; for (int i = 0; i < 100000; i++) { @@ -92,7 +89,7 @@ TEST(BitVectorBenchmarkTest, SelectZeroNonInterleaved90PercentFill) { } } -TEST(BitVectorBenchmarkTest, SelectNonInterleaved) { +TEST(RankSelectBenchmarkTest, Select) { for (size_t n = 8; n <= (1ull << 28); n <<= 2) { std::mt19937_64 rng(42); @@ -100,7 +97,7 @@ TEST(BitVectorBenchmarkTest, SelectNonInterleaved) { for (auto& x : bits) { x = rng(); } - pixie::BitVector bv(bits, n); + pixie::RankSelectSupport<> bv(bits, n); auto max_rank = bv.rank(bv.size()) + 1; @@ -111,7 +108,7 @@ TEST(BitVectorBenchmarkTest, SelectNonInterleaved) { } } -TEST(BitVectorBenchmarkTest, SelectZeroNonInterleaved) { +TEST(RankSelectBenchmarkTest, SelectZero) { for (size_t n = 8; n <= (1ull << 28); n <<= 2) { std::mt19937_64 rng(42); @@ -119,7 +116,7 @@ TEST(BitVectorBenchmarkTest, SelectZeroNonInterleaved) { for (auto& x : bits) { x = rng(); } - pixie::BitVector bv(bits, n); + pixie::RankSelectSupport<> bv(bits, n); auto max_rank0 = bv.rank0(bv.size()) + 1; diff --git a/src/tests/bits_unittests.cc b/src/tests/bits_unittests.cc new file mode 100644 index 0000000..ba0ff9b --- /dev/null +++ b/src/tests/bits_unittests.cc @@ -0,0 +1,353 @@ +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +TEST(Rank512, Ones) { + std::array a{std::numeric_limits::max(), + std::numeric_limits::max(), + std::numeric_limits::max(), + std::numeric_limits::max(), + std::numeric_limits::max(), + std::numeric_limits::max(), + std::numeric_limits::max(), + std::numeric_limits::max()}; + for (size_t i = 0; i < 512; ++i) { + auto p = rank_512(a.data(), i); + EXPECT_EQ(p, i); + } +} + +TEST(Rank512, Random) { + std::array a{std::numeric_limits::max(), + std::numeric_limits::max(), + std::numeric_limits::max(), + std::numeric_limits::max(), + std::numeric_limits::max(), + std::numeric_limits::max(), + std::numeric_limits::max(), + std::numeric_limits::max()}; + + std::mt19937_64 rng(42); + for (size_t t = 0; t < 1000; ++t) { + for (size_t i = 0; i < 8; ++i) { + a[i] = rng(); + } + size_t rank = 0; + for (size_t i = 0; i < 512; ++i) { + auto p = rank_512(a.data(), i); + ASSERT_EQ(p, rank); + rank += 1 & (a[i >> 6] >> (i & 63)); + } + auto p = rank_512(a.data(), 512); + ASSERT_EQ(p, rank); + } +} + +TEST(Select64, Ones) { + uint64_t x = std::numeric_limits::max(); + for (size_t i = 0; i < 64; ++i) { + auto p = select_64(x, i); + EXPECT_EQ(p, i); + } +} + +TEST(Select64, Random) { + uint64_t a; + + std::mt19937_64 rng(42); + for (size_t t = 0; t < 1000; ++t) { + a = rng(); + size_t rank = 0; + for (size_t i = 0; i < 64; ++i) { + if (1 & (a >> i)) { + auto p = select_64(a, rank++); + ASSERT_EQ(p, i); + } + } + } +} + +TEST(Select512, Ones) { + std::array a{std::numeric_limits::max(), + std::numeric_limits::max(), + std::numeric_limits::max(), + std::numeric_limits::max(), + std::numeric_limits::max(), + std::numeric_limits::max(), + std::numeric_limits::max(), + std::numeric_limits::max()}; + for (size_t i = 0; i < 512; ++i) { + auto p = select_512(a.data(), i); + EXPECT_EQ(p, i); + } +} + +TEST(SelectZero512, Zeros) { + std::array a{0, 0, 0, 0, 0, 0, 0, 0}; + for (size_t i = 0; i < 512; ++i) { + auto p = select0_512(a.data(), i); + EXPECT_EQ(p, i); + } +} + +TEST(Select512, Random) { + std::array a{std::numeric_limits::max(), + std::numeric_limits::max(), + std::numeric_limits::max(), + std::numeric_limits::max(), + std::numeric_limits::max(), + std::numeric_limits::max(), + std::numeric_limits::max(), + std::numeric_limits::max()}; + + std::mt19937_64 rng(42); + for (size_t t = 0; t < 1000; ++t) { + for (auto& x : a) { + x = rng(); + } + size_t rank = 0; + for (size_t i = 0; i < 512; ++i) { + if (1 & (a[i >> 6] >> (i & 63))) { + auto p = select_512(a.data(), rank++); + ASSERT_EQ(p, i); + } + } + } +} + +TEST(SelectZero512, Random) { + std::array a{std::numeric_limits::max(), + std::numeric_limits::max(), + std::numeric_limits::max(), + std::numeric_limits::max(), + std::numeric_limits::max(), + std::numeric_limits::max(), + std::numeric_limits::max(), + std::numeric_limits::max()}; + + std::mt19937_64 rng(42); + for (size_t t = 0; t < 1000; ++t) { + for (auto& x : a) { + x = rng(); + } + size_t rank = 0; + for (size_t i = 0; i < 512; ++i) { + if ((1 & (a[i >> 6] >> (i & 63))) == 0) { + auto p = select0_512(a.data(), rank++); + ASSERT_EQ(p, i); + } + } + } +} + +TEST(Select512, RankCompativility) { + std::array a{std::numeric_limits::max(), + std::numeric_limits::max(), + std::numeric_limits::max(), + std::numeric_limits::max(), + std::numeric_limits::max(), + std::numeric_limits::max(), + std::numeric_limits::max(), + std::numeric_limits::max()}; + + std::mt19937_64 rng(42); + for (size_t t = 0; t < 1000; ++t) { + for (auto& x : a) { + x = rng(); + } + size_t rank = 0; + for (size_t i = 0; i < 512; ++i) { + if (1 & (a[i >> 6] >> (i & 63))) { + auto p = select_512(a.data(), rank++); + ASSERT_EQ(p, i); + auto r = rank_512(a.data(), p); + ASSERT_EQ(r + 1, rank); + r = rank_512(a.data(), p + 1); + ASSERT_EQ(r, rank); + } + } + } +} + +TEST(LowerBound4x64, Random) { + std::vector x(8); + std::mt19937_64 rng(42); + for (size_t i = 0; i < 1000; i++) { + uint64_t y = rng(); + uint16_t cnt = 0; + bool fl = 1; + for (size_t j = 0; j < 4; j++) { + x[j] = rng(); + fl &= x[j] < y; + cnt += fl; + } + if (cnt < 4) { + ASSERT_EQ(lower_bound_4x64(x.data(), y), cnt); + } else { + ASSERT_GE(lower_bound_4x64(x.data(), y), cnt); + } + } +} + +TEST(LowerBound8x64, Random) { + std::vector x(8); + std::mt19937_64 rng(42); + for (size_t i = 0; i < 1000; i++) { + uint64_t y = rng(); + uint16_t cnt = 0; + bool fl = 1; + for (size_t j = 0; j < 8; j++) { + x[j] = rng(); + fl &= x[j] < y; + cnt += fl; + } + if (cnt < 8) { + ASSERT_EQ(lower_bound_8x64(x.data(), y), cnt); + } else { + ASSERT_GE(lower_bound_8x64(x.data(), y), cnt); + } + } +} + +TEST(LowerBoundDelta4x64, Random) { + std::vector x(4); + uint64_t dlt_array[4]; + std::mt19937_64 rng(42); + for (size_t i = 0; i < 100000; i++) { + uint64_t y = rng(); + uint64_t dlt_scalar = rng(); + uint16_t cnt = 0; + bool fl = 1; + for (size_t j = 0; j < 4; j++) { + dlt_array[j] = rng(); + x[j] = rng(); + fl &= dlt_scalar + dlt_array[j] - x[j] < y; + cnt += fl; + } + if (cnt < 4) { + ASSERT_EQ(lower_bound_delta_4x64(x.data(), y, dlt_array, dlt_scalar), + cnt); + } else { + ASSERT_GE(lower_bound_delta_4x64(x.data(), y, dlt_array, dlt_scalar), + cnt); + } + } +} + +TEST(LowerBoundDelta8x64, Random) { + std::vector x(8); + uint64_t dlt_array[8]; + std::mt19937_64 rng(42); + for (size_t i = 0; i < 100000; i++) { + uint64_t y = rng(); + uint64_t dlt_scalar = rng(); + uint16_t cnt = 0; + bool fl = 1; + for (size_t j = 0; j < 8; j++) { + dlt_array[j] = rng(); + x[j] = rng(); + fl &= dlt_scalar + dlt_array[j] - x[j] < y; + cnt += fl; + } + if (cnt < 8) { + ASSERT_EQ(lower_bound_delta_8x64(x.data(), y, dlt_array, dlt_scalar), + cnt); + } else { + ASSERT_GE(lower_bound_delta_8x64(x.data(), y, dlt_array, dlt_scalar), + cnt); + } + } +} + +TEST(LowerBound32x16, Random) { + std::vector x(32); + std::mt19937 rng(42); + for (size_t i = 0; i < 1000; i++) { + uint16_t y = rng(); + uint16_t cnt = 0; + for (size_t j = 0; j < 32; j++) { + x[j] = rng(); + cnt += x[j] < y; + } + ASSERT_EQ(lower_bound_32x16(x.data(), y), cnt); + } +} + +TEST(LowerBoundDelta32x16, Random) { + std::vector x(32); + uint16_t dlt_array[32]; + std::mt19937 rng(42); + for (size_t i = 0; i < 100000; i++) { + uint16_t y = rng(); + uint16_t dlt_scalar = rng(); + uint16_t cnt = 0; + for (size_t j = 0; j < 32; j++) { + x[j] = rng(); + if (dlt_scalar < x[j]) { + dlt_array[j] = + x[j] - dlt_scalar + rng() % ((1 << 16) - x[j] + dlt_scalar); + } else { + dlt_array[j] = rng() % ((1 << 16) - dlt_scalar); + } + cnt += dlt_array[j] + dlt_scalar - x[j] < y; + } + ASSERT_EQ(lower_bound_delta_32x16(x.data(), y, dlt_array, dlt_scalar), cnt); + } +} + +TEST(Popcount64x4, Random) { + std::vector x(32); + std::vector y(32); + std::mt19937 rng(42); + for (size_t i = 0; i < 1000; i++) { + for (size_t j = 0; j < 32; j++) { + x[j] = rng(); + } + popcount_64x4(x.data(), y.data()); + for (size_t j = 0; j < 32; ++j) { + uint8_t a = x[j] & 0x0F; + ASSERT_EQ(y[j] & 0x0F, std::popcount(a)); + a = x[j] >> 4; + ASSERT_EQ(y[j] >> 4, std::popcount(a)); + } + } +} + +TEST(Popcount32x8, Random) { + std::vector x(32); + std::vector y(32); + std::mt19937 rng(42); + for (size_t i = 0; i < 1000; i++) { + for (size_t j = 0; j < 32; j++) { + x[j] = rng(); + } + popcount_32x8(x.data(), y.data()); + for (size_t j = 0; j < 32; ++j) { + ASSERT_EQ(y[j], std::popcount(x[j])); + } + } +} + +TEST(RankParallel32x8, Random) { + std::vector x(32); + std::vector sum(32); + std::vector y(32); + std::mt19937 rng(42); + for (size_t i = 0; i < 1000; i++) { + x[0] = rng(); + sum[0] = std::popcount(x[0]); + for (size_t j = 1; j < 32; j++) { + x[j] = rng(); + sum[j] = sum[j - 1] + std::popcount(x[j]); + } + rank_32x8(x.data(), y.data()); + ASSERT_EQ(std::equal(y.begin(), y.end(), sum.begin()), true); + } +} diff --git a/src/tests/bp_tree_tests.cpp b/src/tests/bp_tree_tests.cpp deleted file mode 100644 index 8ad1d1f..0000000 --- a/src/tests/bp_tree_tests.cpp +++ /dev/null @@ -1,119 +0,0 @@ -#include "pixie/bp_tree.h" - -#include -#include - -#include -#include - -using BPTree = pixie::BPTree; -using Node = BPTree::Node; -using pixie::adj_to_bp; - -bool operator==(const AdjListNode& a, const Node& b) { - return a.number == b.number; -} - -bool operator==(Node& b, const AdjListNode& a) { - return a.number == b.number; -} - -TEST(BpTreeTest, Basic) { - std::vector> adj = {{0, 1}, {0, 2}, {1, 3}, {2, 4}, {3}}; - size_t tree_size = 5; - - std::vector bp = adj_to_bp(tree_size, adj); - - BPTree bp_tree(bp, 5); - AdjListTree debug_tree(adj); - - Node cur = bp_tree.root(); - AdjListNode debug = debug_tree.root(); - for (size_t i = 0; i < tree_size - 1; i++) { - EXPECT_EQ(cur, debug); - cur = bp_tree.child(cur, 0); - debug = debug_tree.child(debug, 0); - } - EXPECT_EQ(cur, debug); -} - -TEST(BpTreeTest, RandomTreeDFS) { - for (size_t tree_size = 8; tree_size < (1 << 22); tree_size <<= 1) { - std::mt19937_64 rng(42); - std::vector> adj = generate_random_tree(tree_size, rng); - adj = dfs_order(tree_size, adj); - std::vector bp = adj_to_bp(tree_size, adj); - BPTree bp_tree(bp, tree_size); - AdjListTree debug_tree(adj); - - std::stack> st; - - st.push({bp_tree.root(), debug_tree.root()}); - - while (!st.empty()) { - auto cur = st.top().first; - auto debug = st.top().second; - st.pop(); - EXPECT_EQ(cur, debug); - EXPECT_EQ(bp_tree.parent(cur), debug_tree.parent(debug)); - - if (cur.number > 0) { - EXPECT_EQ(bp_tree.is_last_child(cur), debug_tree.is_last_child(debug)); - } - size_t deg = bp_tree.degree(cur); - EXPECT_EQ(deg, debug_tree.degree(debug)); - EXPECT_EQ(bp_tree.is_leaf(cur), debug_tree.is_leaf(debug)); - - if (deg == 0) { - continue; - } - auto child = bp_tree.first_child(cur); - auto debug_child = debug_tree.first_child(debug); - st.push({child, debug_child}); - for (size_t i = 1; i < deg; i++) { - child = bp_tree.next_sibling(child); - st.push({child, debug_tree.child(debug, i)}); - } - } - } -} - -TEST(BpTreeTest, RandomTreeBFS) { - for (size_t tree_size = 8; tree_size < (1 << 22); tree_size <<= 1) { - std::mt19937_64 rng(42); - std::vector> adj = generate_random_tree(tree_size, rng); - adj = dfs_order(tree_size, adj); - std::vector bp = adj_to_bp(tree_size, adj); - BPTree bp_tree(bp, tree_size); - AdjListTree debug_tree(adj); - - std::queue> st; - - st.push({bp_tree.root(), debug_tree.root()}); - - while (!st.empty()) { - auto cur = st.front().first; - auto debug = st.front().second; - st.pop(); - EXPECT_EQ(bp_tree.parent(cur), debug_tree.parent(debug)); - - if (cur.number > 0) { - EXPECT_EQ(bp_tree.is_last_child(cur), debug_tree.is_last_child(debug)); - } - size_t deg = bp_tree.degree(cur); - EXPECT_EQ(deg, debug_tree.degree(debug)); - EXPECT_EQ(bp_tree.is_leaf(cur), debug_tree.is_leaf(debug)); - - if (deg == 0) { - continue; - } - auto child = bp_tree.first_child(cur); - auto debug_child = debug_tree.first_child(debug); - st.push({child, debug_child}); - for (size_t i = 1; i < deg; i++) { - child = bp_tree.next_sibling(child); - st.push({child, debug_tree.child(debug, i)}); - } - } - } -} diff --git a/src/tests/dfuds_tree_tests.cpp b/src/tests/dfuds_tree_tests.cpp deleted file mode 100644 index a83f416..0000000 --- a/src/tests/dfuds_tree_tests.cpp +++ /dev/null @@ -1,121 +0,0 @@ -#include "pixie/dfuds_tree.h" - -#include -#include - -#include -#include - -using DFUDSTree = pixie::DFUDSTree; -using Node = DFUDSTree::Node; -using pixie::adj_to_dfuds; - -bool operator==(const AdjListNode& a, const DFUDSTree::Node& b) { - return a.number == b.number; -} - -bool operator==(const DFUDSTree::Node& b, const AdjListNode& a) { - return a.number == b.number; -} - -TEST(DfudsTreeTest, Basic) { - std::vector> adj = {{0, 1}, {0, 2}, {1, 3}, {2, 4}, {3}}; - size_t tree_size = 5; - - std::vector dfuds = adj_to_dfuds(tree_size, adj); - - DFUDSTree dfuds_tree(dfuds, 5); - AdjListTree debug_tree(adj); - - Node cur = dfuds_tree.root(); - AdjListNode debug = debug_tree.root(); - for (size_t i = 0; i < tree_size - 1; i++) { - EXPECT_EQ(cur, debug); - cur = dfuds_tree.child(cur, 0); - debug = debug_tree.child(debug, 0); - } - EXPECT_EQ(cur, debug); -} - -TEST(DfudsTreeTest, RandomTreeDFS) { - for (size_t tree_size = 8; tree_size < (1 << 22); tree_size <<= 1) { - std::mt19937_64 rng(42); - std::vector> adj = generate_random_tree(tree_size, rng); - adj = dfs_order(tree_size, adj); - std::vector dfuds = adj_to_dfuds(tree_size, adj); - DFUDSTree dfuds_tree(dfuds, tree_size); - AdjListTree debug_tree(adj); - - std::stack> st; - - st.push({dfuds_tree.root(), debug_tree.root()}); - - while (!st.empty()) { - auto cur = st.top().first; - auto debug = st.top().second; - st.pop(); - EXPECT_EQ(cur, debug); - EXPECT_EQ(dfuds_tree.parent(cur), debug_tree.parent(debug)); - - if (cur.number > 0) { - EXPECT_EQ(dfuds_tree.is_last_child(cur), - debug_tree.is_last_child(debug)); - } - size_t deg = dfuds_tree.degree(cur); - EXPECT_EQ(deg, debug_tree.degree(debug)); - EXPECT_EQ(dfuds_tree.is_leaf(cur), debug_tree.is_leaf(debug)); - - if (deg == 0) { - continue; - } - auto child = dfuds_tree.first_child(cur); - auto debug_child = debug_tree.first_child(debug); - st.push({child, debug_child}); - for (size_t i = 1; i < deg; i++) { - child = dfuds_tree.next_sibling(child); - st.push({child, debug_tree.child(debug, i)}); - } - } - } -} - -TEST(DfudsTreeTest, RandomTreeBFS) { - for (size_t tree_size = 8; tree_size < (1 << 22); tree_size <<= 1) { - std::mt19937_64 rng(42); - std::vector> adj = generate_random_tree(tree_size, rng); - adj = dfs_order(tree_size, adj); - std::vector dfuds = adj_to_dfuds(tree_size, adj); - DFUDSTree dfuds_tree(dfuds, tree_size); - AdjListTree debug_tree(adj); - - std::queue> st; - - st.push({dfuds_tree.root(), debug_tree.root()}); - - while (!st.empty()) { - auto cur = st.front().first; - auto debug = st.front().second; - st.pop(); - EXPECT_EQ(dfuds_tree.parent(cur), debug_tree.parent(debug)); - - if (cur.number > 0) { - EXPECT_EQ(dfuds_tree.is_last_child(cur), - debug_tree.is_last_child(debug)); - } - size_t deg = dfuds_tree.degree(cur); - EXPECT_EQ(deg, debug_tree.degree(debug)); - EXPECT_EQ(dfuds_tree.is_leaf(cur), debug_tree.is_leaf(debug)); - - if (deg == 0) { - continue; - } - auto child = dfuds_tree.first_child(cur); - auto debug_child = debug_tree.first_child(debug); - st.push({child, debug_child}); - for (size_t i = 1; i < deg; i++) { - child = dfuds_tree.next_sibling(child); - st.push({child, debug_tree.child(debug, i)}); - } - } - } -} diff --git a/src/tests/louds_tree_tests.cpp b/src/tests/louds_tree_tests.cpp deleted file mode 100644 index 8f03e4d..0000000 --- a/src/tests/louds_tree_tests.cpp +++ /dev/null @@ -1,113 +0,0 @@ -#include -#include -#include - -#include -#include -#include -#include - -using pixie::LoudsNode; -using pixie::LoudsTree; - -TEST(LoudsTreeTest, Basic) { - std::vector> adj = {{0, 1}, {0, 2}, {1, 3}, {2, 4}, {3}}; - size_t tree_size = 5; - - std::vector louds = adj_to_louds(tree_size, adj); - - LoudsTree louds_tree(louds, 5); - AdjListTree debug_tree(adj); - - LoudsNode cur = louds_tree.root(); - AdjListNode debug = debug_tree.root(); - for (size_t i = 0; i < tree_size - 1; i++) { - EXPECT_EQ(cur, debug); - cur = louds_tree.child(cur, 0); - debug = debug_tree.child(debug, 0); - } - EXPECT_EQ(cur, debug); -} - -TEST(LoudsTreeTest, RandomTreeDFS) { - for (size_t tree_size = 8; tree_size < (1 << 22); tree_size <<= 1) { - std::mt19937_64 rng(42); - std::vector> adj = generate_random_tree(tree_size, rng); - adj = bfs_order(tree_size, adj); - std::vector louds = adj_to_louds(tree_size, adj); - LoudsTree louds_tree(louds, tree_size); - AdjListTree debug_tree(adj); - - std::stack> st; - - st.push({louds_tree.root(), debug_tree.root()}); - - while (!st.empty()) { - auto cur = st.top().first; - auto debug = st.top().second; - st.pop(); - EXPECT_EQ(cur, debug); - EXPECT_EQ(louds_tree.parent(cur), debug_tree.parent(debug)); - EXPECT_EQ(louds_tree.is_last_child(cur), debug_tree.is_last_child(debug)); - - if (!debug_tree.is_last_child(debug)) { - EXPECT_EQ(louds_tree.next_sibling(cur), debug_tree.next_sibling(debug)); - } - - size_t deg = louds_tree.degree(cur); - EXPECT_EQ(deg, debug_tree.degree(debug)); - - for (size_t i = 0; i < deg; i++) { - st.push({louds_tree.child(cur, i), debug_tree.child(debug, i)}); - } - } - } -} - -TEST(LoudsTreeTest, RandomTreeDFSWithZeroExtraMemory) { - for (size_t tree_size = 8; tree_size < (1 << 22); tree_size <<= 1) { - std::mt19937_64 rng(42); - std::vector> adj = generate_random_tree(tree_size, rng); - adj = bfs_order(tree_size, adj); - std::vector louds = adj_to_louds(tree_size, adj); - LoudsTree louds_tree(louds, tree_size); - AdjListTree debug_tree(adj); - - LoudsNode cur = louds_tree.root(); - AdjListNode debug = debug_tree.root(); - - bool above = 1; - size_t cnt_visited = 1; - while (true) { - if (above) { - ASSERT_EQ(louds_tree.is_leaf(cur), debug_tree.is_leaf(debug)); - if (louds_tree.is_leaf(cur)) { - above = 0; - } else { - cur = louds_tree.first_child(cur); - debug = debug_tree.first_child(debug); - ASSERT_EQ(cur, debug); - cnt_visited++; - } - } else { - ASSERT_EQ(louds_tree.is_last_child(cur), - debug_tree.is_last_child(debug)); - if (louds_tree.is_last_child(cur)) { - cur = louds_tree.parent(cur); - debug = debug_tree.parent(debug); - ASSERT_EQ(cur, debug); - if (louds_tree.is_root(cur)) { - break; - } - } else { - cur = louds_tree.next_sibling(cur); - debug = debug_tree.next_sibling(debug); - ASSERT_EQ(cur, debug); - above = 1; - cnt_visited++; - } - } - } - ASSERT_EQ(tree_size, cnt_visited); - } -} diff --git a/src/tests/rank_select_tests.cpp b/src/tests/rank_select_tests.cpp new file mode 100644 index 0000000..9dedb89 --- /dev/null +++ b/src/tests/rank_select_tests.cpp @@ -0,0 +1,86 @@ +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace { + +template +class RankSelectSpecificationTest : public testing::Test {}; + +using RankSelectImplementations = testing::Types>; +TYPED_TEST_SUITE(RankSelectSpecificationTest, RankSelectImplementations); + +TYPED_TEST(RankSelectSpecificationTest, EmptySequence) { + const std::vector words; + const TypeParam bits(words, 0); + + EXPECT_TRUE(bits.empty()); + EXPECT_EQ(bits.size(), 0u); + EXPECT_EQ(bits.rank(0), 0u); + EXPECT_EQ(bits.rank0(0), 0u); + EXPECT_EQ(bits.select(1), 0u); + EXPECT_EQ(bits.select0(1), 0u); + EXPECT_EQ(bits.to_string(), ""); +} + +TYPED_TEST(RankSelectSpecificationTest, RankSelectAndAccessMatchPackedBits) { + constexpr std::size_t sizes[] = {1, 2, 63, 64, 65, 127, + 495, 496, 497, 511, 512, 513, + 1024, 4097, 65535, 65536, 65537, 131073}; + std::mt19937_64 rng(42); + + for (const std::size_t size : sizes) { + std::vector words((size + 63) / 64); + for (auto& word : words) { + word = rng(); + } + const TypeParam bits(words, size); + ASSERT_FALSE(bits.empty()); + ASSERT_EQ(bits.size(), size); + ASSERT_TRUE(bits.supports_select1()); + ASSERT_TRUE(bits.supports_select0()); + + std::size_t ones = 0; + std::size_t zeros = 0; + std::string expected; + expected.reserve(size); + for (std::size_t position = 0; position < size; ++position) { + const bool value = ((words[position / 64] >> (position % 64)) & 1u) != 0; + EXPECT_EQ(bits.rank(position), ones) << "size=" << size; + EXPECT_EQ(bits.rank0(position), zeros) << "size=" << size; + EXPECT_EQ(bits[position], value) << "size=" << size; + expected.push_back(value ? '1' : '0'); + if (value) { + ++ones; + EXPECT_EQ(bits.select(ones), position) << "size=" << size; + } else { + ++zeros; + EXPECT_EQ(bits.select0(zeros), position) << "size=" << size; + } + } + EXPECT_EQ(bits.rank(size), ones); + EXPECT_EQ(bits.rank0(size), zeros); + EXPECT_EQ(bits.select(ones + 1), size); + EXPECT_EQ(bits.select0(zeros + 1), size); + EXPECT_EQ(bits.to_string(), expected); + } +} + +TEST(RankSelectSupportTest, AcceptsStorageSourceWithoutCopying) { + pixie::AlignedStorage source(64); + source.writable_words64()[0] = 0b101101; + const pixie::RankSelectSupport<> support(source, 6); + + EXPECT_EQ(support.rank(6), 4u); + EXPECT_EQ(support.select(4), 5u); + source.writable_words64()[0] = 0b000001; + EXPECT_EQ(support[2], 0); +} + +} // namespace diff --git a/src/tests/rank_select_unittests.cc b/src/tests/rank_select_unittests.cc new file mode 100644 index 0000000..c31724d --- /dev/null +++ b/src/tests/rank_select_unittests.cc @@ -0,0 +1,383 @@ +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +using RankSelectSupport = pixie::RankSelectSupport<>; + +TEST(RankSelectSupportTest, Basic) { + std::vector bits(8, 0); + bits[0] = 0b101010101; + RankSelectSupport bv(bits, 9); + EXPECT_EQ(bv.size(), 9); + EXPECT_EQ(bv[0], 1); + EXPECT_EQ(bv[1], 0); + EXPECT_EQ(bv[2], 1); + EXPECT_EQ(bv[3], 0); + EXPECT_EQ(bv[4], 1); + EXPECT_EQ(bv[5], 0); + EXPECT_EQ(bv[6], 1); + EXPECT_EQ(bv[7], 0); + EXPECT_EQ(bv[8], 1); +} + +TEST(RankSelectSupportTest, MultipleWords) { + std::vector bits(8, 0); + bits[0] = 0xAAAAAAAAAAAAAAAA; + bits[1] = 0x5555555555555555; + RankSelectSupport bv(bits, 128); + EXPECT_EQ(bv.size(), 128); + + // First word: 0xAAAAAAAAAAAAAAAA (alternating 1s and 0s, starting with 0) + for (size_t i = 0; i < 64; i++) { + EXPECT_EQ(bv[i], (i % 2 == 1)); + } + + // Second word: 0x5555555555555555 (alternating 0s and 1s, starting with 1) + for (size_t i = 64; i < 128; i++) { + EXPECT_EQ(bv[i], (i % 2 == 0)); + } +} + +TEST(RankSelectSupportTest, ToString) { + std::vector bits(8, 0); + bits[0] = 0b10101010101; + RankSelectSupport bv(bits, 11); + EXPECT_EQ(bv.to_string(), "10101010101"); +} + +TEST(RankSelectSupportTest, RankBasic) { + std::vector bits(8, 0); + bits[0] = 0b10110; + RankSelectSupport bv(bits, 5); + + EXPECT_EQ(bv.rank(0), 0); // No bits + EXPECT_EQ(bv.rank(1), 0); // 0 + EXPECT_EQ(bv.rank(2), 1); // 10 + EXPECT_EQ(bv.rank(3), 2); // 110 + EXPECT_EQ(bv.rank(4), 2); // 0110 + EXPECT_EQ(bv.rank(5), 3); // 10110 +} + +TEST(RankSelectSupportTest, RankWithZeros) { + std::vector bits(8, 0); + RankSelectSupport bv(bits, 5); + + for (size_t i = 0; i <= 5; i++) { + EXPECT_EQ(bv.rank(i), 0); + } +} + +TEST(RankSelectSupportTest, SelectBasic) { + std::vector bits(8, 0); + bits[0] = 0b1100010110010110; + RankSelectSupport bv(bits, 16); + + EXPECT_EQ(bv.select(1), 1); + EXPECT_EQ(bv.select(2), 2); + EXPECT_EQ(bv.select(3), 4); + EXPECT_EQ(bv.select(4), 7); + EXPECT_EQ(bv.select(5), 8); + EXPECT_EQ(bv.select(6), 10); + EXPECT_EQ(bv.select(7), 14); + EXPECT_EQ(bv.select(8), 15); +} + +TEST(RankSelectSupportTest, MainRankTest) { + std::mt19937_64 rng(42); + std::vector bits(65536 * 32); + for (size_t i = 0; i < 65536 * 32; i++) { + bits[i] = rng(); + } + + RankSelectSupport bv(bits, 65536 * 32 * 64); + uint64_t rank = 0; + for (size_t i = 0; i < bv.size(); ++i) { + ASSERT_EQ(rank, bv.rank(i)); + rank += bv[i]; + } +} + +TEST(RankSelectSupportTest, MainSelectTest) { + std::mt19937_64 rng(42); + std::vector bits(65536 * 32); + for (size_t i = 0; i < 65536 * 32; i++) { + bits[i] = rng(); + } + + RankSelectSupport bv(bits, 65536 * 32 * 64); + uint64_t rank = 0; + + for (size_t i = 0; i < bv.size(); ++i) { + if (bv[i]) { + ASSERT_EQ(bv.select(++rank), i); + ASSERT_EQ(bv.rank(i), rank - 1); + ASSERT_EQ(bv.rank(i + 1), rank); + } + } +} + +TEST(RankSelectSupportTest, RankZeroBasic) { + std::vector bits(8, 0); + bits[0] = 0b10110; + RankSelectSupport bv(bits, 10); + + EXPECT_EQ(bv.rank0(0), 0); + EXPECT_EQ(bv.rank0(1), 1); + EXPECT_EQ(bv.rank0(2), 1); + EXPECT_EQ(bv.rank0(3), 1); + EXPECT_EQ(bv.rank0(4), 2); + EXPECT_EQ(bv.rank0(5), 2); +} + +TEST(RankSelectSupportTest, RankZeroWithOnes) { + std::vector bits(8, 0); + RankSelectSupport bv(bits, 5); + + for (size_t i = 0; i <= 5; i++) { + EXPECT_EQ(bv.rank0(i), i); + } +} + +TEST(RankSelectSupportTest, SelectZeroBasic) { + std::vector bits(8, 0); + bits[0] = 0b1100010110010110; + RankSelectSupport bv(bits, 16); + + EXPECT_EQ(bv.select0(1), 0); + EXPECT_EQ(bv.select0(2), 3); + EXPECT_EQ(bv.select0(3), 5); + EXPECT_EQ(bv.select0(4), 6); + EXPECT_EQ(bv.select0(5), 9); + EXPECT_EQ(bv.select0(6), 11); + EXPECT_EQ(bv.select0(7), 12); + EXPECT_EQ(bv.select0(8), 13); +} + +TEST(RankSelectSupportTest, EmptyAndZeroRankGuards) { + RankSelectSupport default_bv; + EXPECT_EQ(default_bv.size(), 0u); + EXPECT_EQ(default_bv.rank(0), 0u); + EXPECT_EQ(default_bv.rank0(0), 0u); + EXPECT_EQ(default_bv.select(0), 0u); + EXPECT_EQ(default_bv.select(1), 0u); + EXPECT_EQ(default_bv.select0(0), 0u); + EXPECT_EQ(default_bv.select0(1), 0u); + EXPECT_EQ(default_bv.to_string(), ""); + + std::vector empty; + RankSelectSupport bv(std::span(empty), 0); + + EXPECT_EQ(bv.size(), 0u); + EXPECT_EQ(bv.rank(0), 0u); + EXPECT_EQ(bv.rank0(0), 0u); + EXPECT_EQ(bv.select(0), 0u); + EXPECT_EQ(bv.select(1), 0u); + EXPECT_EQ(bv.select0(0), 0u); + EXPECT_EQ(bv.select0(1), 0u); + EXPECT_EQ(bv.to_string(), ""); + + std::vector bits(1, 0b10110); + RankSelectSupport non_empty(std::span(bits), 5); + EXPECT_EQ(non_empty.select(0), 0u); + EXPECT_EQ(non_empty.select0(0), 0u); +} + +TEST(RankSelectSupportTest, ExactShortSpanRankSelect) { + std::vector bits = {0b1100010110010110}; + RankSelectSupport bv(bits, 16); + + EXPECT_EQ(bv.rank(16), 8); + EXPECT_EQ(bv.rank0(16), 8); + EXPECT_EQ(bv.select(1), 1); + EXPECT_EQ(bv.select(8), 15); + EXPECT_EQ(bv.select(9), bv.size()); + EXPECT_EQ(bv.select0(1), 0); + EXPECT_EQ(bv.select0(8), 13); + EXPECT_EQ(bv.select0(9), bv.size()); +} + +TEST(RankSelectSupportTest, OptionalSelectSupport) { + std::vector bits = {0b1100010110010110}; + + RankSelectSupport select0_only(bits, 16, + RankSelectSupport::SelectSupport::kSelect0); + EXPECT_FALSE(select0_only.supports_select1()); + EXPECT_TRUE(select0_only.supports_select0()); + EXPECT_EQ(select0_only.rank(16), 8); + EXPECT_EQ(select0_only.rank0(16), 8); + EXPECT_EQ(select0_only.select(1), select0_only.size()); + EXPECT_EQ(select0_only.select0(1), 0); + EXPECT_EQ(select0_only.select0(8), 13); + + RankSelectSupport hinted_both(bits, 16, + RankSelectSupport::SelectSupport::kBoth, 8); + EXPECT_TRUE(hinted_both.supports_select1()); + EXPECT_TRUE(hinted_both.supports_select0()); + EXPECT_EQ(hinted_both.select(1), 1); + EXPECT_EQ(hinted_both.select(8), 15); + EXPECT_EQ(hinted_both.select0(1), 0); + EXPECT_EQ(hinted_both.select0(8), 13); + EXPECT_THROW((RankSelectSupport(bits, 16, + RankSelectSupport::SelectSupport::kBoth, 17)), + std::invalid_argument); + + RankSelectSupport select1_only(bits, 16, + RankSelectSupport::SelectSupport::kSelect1); + EXPECT_TRUE(select1_only.supports_select1()); + EXPECT_FALSE(select1_only.supports_select0()); + EXPECT_EQ(select1_only.rank(16), 8); + EXPECT_EQ(select1_only.rank0(16), 8); + EXPECT_EQ(select1_only.select(1), 1); + EXPECT_EQ(select1_only.select(8), 15); + EXPECT_EQ(select1_only.select0(1), select1_only.size()); + + RankSelectSupport rank_only(bits, 16, + RankSelectSupport::SelectSupport::kNone); + EXPECT_FALSE(rank_only.supports_select1()); + EXPECT_FALSE(rank_only.supports_select0()); + EXPECT_EQ(rank_only.rank(16), 8); + EXPECT_EQ(rank_only.rank0(16), 8); + EXPECT_EQ(rank_only.select(0), 0); + EXPECT_EQ(rank_only.select0(0), 0); + EXPECT_EQ(rank_only.select(1), rank_only.size()); + EXPECT_EQ(rank_only.select0(1), rank_only.size()); + + std::vector zero_words(512, 0); + RankSelectSupport large_select0_only( + zero_words, zero_words.size() * 64, + RankSelectSupport::SelectSupport::kSelect0); + EXPECT_EQ(large_select0_only.select(1), large_select0_only.size()); + EXPECT_EQ(large_select0_only.select0(1), 0); + EXPECT_EQ(large_select0_only.select0(16384), 16383); + EXPECT_EQ(large_select0_only.select0(16385), 16384); + EXPECT_EQ(large_select0_only.select0(32768), 32767); + + RankSelectSupport large_hinted_select0( + zero_words, zero_words.size() * 64, + RankSelectSupport::SelectSupport::kSelect0, 0); + EXPECT_EQ(large_hinted_select0.select(1), large_hinted_select0.size()); + EXPECT_EQ(large_hinted_select0.select0(1), 0); + EXPECT_EQ(large_hinted_select0.select0(32768), 32767); + + std::vector one_words(512, ~uint64_t{0}); + RankSelectSupport large_select1_only( + one_words, one_words.size() * 64, + RankSelectSupport::SelectSupport::kSelect1); + EXPECT_EQ(large_select1_only.select0(1), large_select1_only.size()); + EXPECT_EQ(large_select1_only.select(1), 0); + EXPECT_EQ(large_select1_only.select(16384), 16383); + EXPECT_EQ(large_select1_only.select(16385), 16384); + EXPECT_EQ(large_select1_only.select(32768), 32767); +} + +TEST(RankSelectSupportTest, ThrowsWhenOneCountHintUnderestimatesSamples) { + std::vector one_words(512, ~uint64_t{0}); + + EXPECT_THROW( + (RankSelectSupport(one_words, one_words.size() * 64, + RankSelectSupport::SelectSupport::kSelect1, 0)), + std::invalid_argument); +} + +TEST(RankSelectSupportTest, ShortSpanUsesScalarRankSelectFallbacks) { + std::vector one_in_second_word = {0, 1}; + RankSelectSupport ones(std::span(one_in_second_word), 65); + EXPECT_EQ(ones.rank(64), 0u); + EXPECT_EQ(ones.rank(65), 1u); + EXPECT_EQ(ones.rank0(65), 64u); + EXPECT_EQ(ones.select(1), 64u); + + std::vector zero_in_second_word = { + std::numeric_limits::max(), 0}; + RankSelectSupport zeros(std::span(zero_in_second_word), 65); + EXPECT_EQ(zeros.rank(65), 64u); + EXPECT_EQ(zeros.rank0(64), 0u); + EXPECT_EQ(zeros.rank0(65), 1u); + EXPECT_EQ(zeros.select0(1), 64u); +} + +TEST(RankSelectSupportTest, ShortSpanScalarRankUsesPartialWordTail) { + std::vector bits = {~uint64_t{0}, 0, 0b101}; + RankSelectSupport bv(std::span(bits), 130); + + EXPECT_EQ(bv.rank(128), 64u); + EXPECT_EQ(bv.rank(129), 65u); + EXPECT_EQ(bv.rank(130), 65u); + EXPECT_EQ(bv.rank0(129), 64u); +} + +TEST(RankSelectSupportTest, IgnoresDirtyPaddingAndTrailingWords) { + std::vector bits = {~uint64_t{0}, ~uint64_t{0}}; + RankSelectSupport bv(bits, 3); + + EXPECT_EQ(bv.size(), 3); + EXPECT_EQ(bv.rank(3), 3); + EXPECT_EQ(bv.rank(128), 3); + EXPECT_EQ(bv.rank0(3), 0); + EXPECT_EQ(bv.rank0(128), 0); + EXPECT_EQ(bv.select(1), 0); + EXPECT_EQ(bv.select(3), 2); + EXPECT_EQ(bv.select(4), bv.size()); + EXPECT_EQ(bv.select0(1), bv.size()); +} + +TEST(RankSelectSupportTest, SelectZeroSkewedDistributionFallback) { + constexpr size_t kWordsPerBasicBlock = 8; + constexpr size_t kBasicBlocksPerSuperBlock = 128; + constexpr size_t kZeroBasicBlocks = 40; + + std::vector bits(kWordsPerBasicBlock * kBasicBlocksPerSuperBlock, + ~uint64_t{0}); + std::fill(bits.begin(), bits.begin() + kZeroBasicBlocks * kWordsPerBasicBlock, + 0); + + RankSelectSupport bv(std::span(bits), bits.size() * 64); + EXPECT_EQ(bv.rank0(bv.size()), kZeroBasicBlocks * 512u); + EXPECT_EQ(bv.select0(1), 0u); + EXPECT_EQ(bv.select0(10000), 9999u); + EXPECT_EQ(bv.select0(kZeroBasicBlocks * 512u), kZeroBasicBlocks * 512u - 1); + EXPECT_EQ(bv.select0(kZeroBasicBlocks * 512u + 1), bv.size()); +} + +TEST(RankSelectSupportTest, MainRankZeroTest) { + std::mt19937_64 rng(42); + std::vector bits(65536 * 32); + for (size_t i = 0; i < bits.size(); i++) { + bits[i] = rng(); + } + + RankSelectSupport bv(bits, bits.size() * 64); + uint64_t zero_count = 0; + for (size_t i = 0; i < bv.size(); ++i) { + ASSERT_EQ(zero_count, bv.rank0(i)); + zero_count += (bv[i] == 0) ? 1 : 0; + } +} + +TEST(RankSelectSupportTest, MainSelectZeroTest) { + std::mt19937_64 rng(42); + std::vector bits(65536 * 32); + for (size_t i = 0; i < bits.size(); i++) { + bits[i] = rng(); + } + + RankSelectSupport bv(bits, bits.size() * 64); + uint64_t zero_rank = 0; + + for (size_t i = 0; i < bv.size(); ++i) { + if (bv[i] == 0) { + zero_rank++; + EXPECT_EQ(bv.select0(zero_rank), i); + EXPECT_EQ(bv.rank0(i), zero_rank - 1); + EXPECT_EQ(bv.rank0(i + 1), zero_rank); + } + } +} diff --git a/src/tests/rmq_tests.cpp b/src/tests/rmq_tests.cpp index 9ab5d72..f31434d 100644 --- a/src/tests/rmq_tests.cpp +++ b/src/tests/rmq_tests.cpp @@ -1,13 +1,7 @@ #include -#include -#include -#include -#include +#include #include - -#ifdef SDSL_SUPPORT -#include -#endif +#include #include #include diff --git a/src/tests/storage_tests.cpp b/src/tests/storage_tests.cpp new file mode 100644 index 0000000..aecee51 --- /dev/null +++ b/src/tests/storage_tests.cpp @@ -0,0 +1,179 @@ +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +template +class StorageSpecificationTest : public testing::Test {}; + +using StorageTypes = + testing::Types; +TYPED_TEST_SUITE(StorageSpecificationTest, StorageTypes); + +template +Storage make_storage(std::span bytes) { + if constexpr (std::same_as) { + Storage result(bytes.size() * 8); + std::copy(bytes.begin(), bytes.end(), result.writable_bytes().begin()); + return result; + } else { + return Storage(bytes); + } +} + +template +concept HasWritableBytes = requires(Storage value) { value.writable_bytes(); }; + +template +concept HasResize = requires(Storage value) { value.resize(1); }; + +TYPED_TEST(StorageSpecificationTest, EmptyStorageHasConsistentViews) { + const std::array bytes{}; + auto storage = make_storage(bytes); + EXPECT_TRUE(storage.empty()); + EXPECT_EQ(storage.size_bytes(), 0u); + EXPECT_EQ(storage.size_bits(), 0u); + EXPECT_TRUE(storage.as_bytes().empty()); + EXPECT_TRUE(storage.view().empty()); +} + +TYPED_TEST(StorageSpecificationTest, SupportsByteAndNestedViews) { + const std::array bytes = {std::byte{0}, std::byte{1}, std::byte{2}, + std::byte{3}, std::byte{4}, std::byte{5}, + std::byte{6}, std::byte{7}}; + auto storage = make_storage(bytes); + auto middle = storage.view(2, 4); + auto nested = middle.view(1, 2); + EXPECT_EQ(middle.as_bytes()[0], std::byte{2}); + EXPECT_EQ(nested.as_bytes()[0], std::byte{3}); + EXPECT_EQ(nested.as_bytes()[1], std::byte{4}); + EXPECT_THROW(storage.view(storage.size_bytes(), 1), std::out_of_range); +} + +TYPED_TEST(StorageSpecificationTest, ProvidesAlignedWordViewsWhenValid) { + alignas(8) const std::array words = {1, 2, 3, 4, + 5, 6, 7, 8}; + const auto bytes = std::as_bytes(std::span(words)); + auto storage = make_storage(bytes); + EXPECT_EQ(storage.as_words16().size(), storage.size_bytes() / 2); + EXPECT_EQ(storage.as_words64()[0], 1u); + EXPECT_THROW(storage.view(1, 2).as_words16(), std::invalid_argument); + EXPECT_THROW(storage.view(0, 3).as_words16(), std::invalid_argument); +} + +TEST(StorageSerializationTest, OwningStorageAndViewSerializeIdentically) { + pixie::AlignedStorage storage(1); + storage.writable_bytes()[0] = std::byte{42}; + pixie::OutputBitStream owning_stream; + pixie::OutputBitStream view_stream; + storage.serialize(owning_stream); + storage.view().serialize(view_stream); + EXPECT_EQ(owning_stream.extract(), view_stream.extract()); +} + +TEST(StorageSerializationTest, ReadOnlyViewRoundTripsAndAdvancesInput) { + pixie::AlignedStorage storage(1); + storage.writable_bytes()[0] = std::byte{42}; + pixie::OutputBitStream stream; + storage.serialize(stream); + const auto serialized_words = stream.extract(); + std::span input = + std::as_bytes(std::span(serialized_words)); + const auto restored = pixie::ReadOnlyStorageView::deserialize(input); + EXPECT_TRUE(std::ranges::equal(restored.as_bytes(), storage.as_bytes())); + EXPECT_TRUE(input.empty()); +} + +TEST(AlignedStorageTest, PadsResizesAndProvidesWritableStorage) { + pixie::AlignedStorage storage(1); + EXPECT_EQ(storage.size_bytes(), pixie::kAlignedStorageLineBytes); + EXPECT_EQ(storage.size_bits(), pixie::kAlignedStorageLineBits); + EXPECT_EQ(reinterpret_cast(storage.as_bytes().data()) % 64, + 0u); + storage.writable_words64()[0] = 42; + EXPECT_EQ(storage.as_words64()[0], 42u); + storage.resize(0); + EXPECT_TRUE(storage.empty()); + EXPECT_GE(storage.allocated_bytes(), storage.size_bytes()); + storage.shrink_to_fit(); +} + +TEST(ReadOnlyStorageViewTest, MutatingOperationsAreNotAvailable) { + static_assert(!HasWritableBytes); + static_assert(!HasResize); +} + +TEST(ReadOnlyStorageViewTest, DeserializeRejectsTruncatedInput) { + std::array bytes{}; + const std::size_t payload_size = 1; + std::memcpy(bytes.data(), &payload_size, sizeof(payload_size)); + std::span input(bytes); + EXPECT_THROW(pixie::ReadOnlyStorageView::deserialize(input), + std::invalid_argument); +} + +TEST(MappedFileTest, MapsContentsAndIsMoveOnly) { + static_assert(!std::is_copy_constructible_v); + const auto path = + std::filesystem::temp_directory_path() / "pixie_mapped_file_test.bin"; + { + std::ofstream output(path, std::ios::binary); + output.write("pixie", 5); + } + pixie::io::MappedFile file(path); + EXPECT_EQ(file.size_bytes(), 5u); + EXPECT_EQ(file.as_bytes()[0], std::byte{'p'}); + std::filesystem::remove(path); +} + +TEST(MappedFileTest, EmptyFileHasAnEmptyView) { + const auto path = std::filesystem::temp_directory_path() / + "pixie_empty_mapped_file_test.bin"; + std::ofstream(path, std::ios::binary); + + pixie::io::MappedFile file(path); + EXPECT_EQ(file.size_bytes(), 0u); + EXPECT_TRUE(file.as_bytes().empty()); + std::filesystem::remove(path); +} + +TEST(MappedFileTest, MoveTransfersMappingOwnership) { + const auto path = std::filesystem::temp_directory_path() / + "pixie_mapped_file_move_test.bin"; + { + std::ofstream output(path, std::ios::binary); + output.write("pixie", 5); + } + + pixie::io::MappedFile source(path); + pixie::io::MappedFile moved(std::move(source)); + EXPECT_TRUE(source.as_bytes().empty()); + EXPECT_EQ(moved.as_bytes()[0], std::byte{'p'}); + + pixie::io::MappedFile assigned(path); + assigned = std::move(moved); + EXPECT_TRUE(moved.as_bytes().empty()); + EXPECT_EQ(assigned.as_bytes()[0], std::byte{'p'}); + std::filesystem::remove(path); +} + +TEST(MappedFileTest, RejectsMissingFile) { + EXPECT_THROW(pixie::io::MappedFile(std::filesystem::temp_directory_path() / + "pixie_missing_mapped_file.bin"), + std::runtime_error); +} + +} // namespace diff --git a/src/tests/test_rmm.cpp b/src/tests/test_rmm.cpp index dc0d7d8..f61beb9 100644 --- a/src/tests/test_rmm.cpp +++ b/src/tests/test_rmm.cpp @@ -1,10 +1,6 @@ #include -#include -#include -#ifdef SDSL_SUPPORT -#include -#endif -#include +#include +#include #include #include @@ -111,19 +107,20 @@ static constexpr size_t kMaxBits = 20000; static constexpr size_t kLongOpsPerCase = 2000; static constexpr size_t kLongMaxBits = 65536; +template static void run_case_and_compare(const std::string& bits, std::mt19937_64& rng, size_t ops_per_case, uint64_t seed) { const bool use_words = std::uniform_int_distribution(0, 1)(rng); - pixie::RmMTree rm; + RmM rm; NaiveRmM nv; auto words = pack_words_lsb_first(bits); if (use_words) { - rm = pixie::RmMTree(std::span(words), bits.size()); + rm = RmM(std::span(words), bits.size()); nv = NaiveRmM(words, bits.size()); } else { - rm = pixie::RmMTree(std::span(words), bits.size()); + rm = RmM(std::span(words), bits.size()); nv = NaiveRmM(bits); } @@ -310,21 +307,28 @@ static void run_case_and_compare(const std::string& bits, } } +template class RmMRandomTest : public ::testing::Test { protected: std::mt19937_64 rng{kSeed}; }; -TEST_F(RmMRandomTest, RandomMixedBits) { +using RmMImplementations = + testing::Types>; +TYPED_TEST_SUITE(RmMRandomTest, RmMImplementations); + +TYPED_TEST(RmMRandomTest, RandomMixedBits) { + auto& rng = this->rng; std::uniform_int_distribution len_u(1, (int)kMaxBits); for (size_t t = 0; t < kRandomCases; ++t) { const size_t n = (size_t)len_u(rng); const std::string bits = random_bits(rng, n); - run_case_and_compare(bits, rng, kOpsPerCase, kSeed); + run_case_and_compare(bits, rng, kOpsPerCase, kSeed); } } -TEST_F(RmMRandomTest, RandomDyckBits) { +TYPED_TEST(RmMRandomTest, RandomDyckBits) { + auto& rng = this->rng; std::uniform_int_distribution len_even(0, (int)(kMaxBits / 2)); for (size_t t = 0; t < kRandomCases; ++t) { size_t m = 1 + (size_t)len_even(rng); @@ -332,11 +336,12 @@ TEST_F(RmMRandomTest, RandomDyckBits) { if (bits.empty()) { bits = "10"; } - run_case_and_compare(bits, rng, kOpsPerCase, kSeed); + run_case_and_compare(bits, rng, kOpsPerCase, kSeed); } } -TEST_F(RmMRandomTest, ShortInputs) { +TYPED_TEST(RmMRandomTest, ShortInputs) { + auto& rng = this->rng; for (size_t n = 1; n <= 8; ++n) { const size_t total = (n ? (1ull << n) : 1ull); for (size_t mask = 0; mask < total; ++mask) { @@ -348,12 +353,12 @@ TEST_F(RmMRandomTest, ShortInputs) { { SCOPED_TRACE(::testing::Message() << "short-inputs:string bits=" << bits); - run_case_and_compare(bits, rng, /*ops_per_case=*/200, kSeed); + run_case_and_compare(bits, rng, /*ops_per_case=*/200, kSeed); } { SCOPED_TRACE(::testing::Message() << "short-inputs:words bits=" << bits); - run_case_and_compare(bits, rng, /*ops_per_case=*/200, kSeed); + run_case_and_compare(bits, rng, /*ops_per_case=*/200, kSeed); } } } @@ -457,7 +462,8 @@ static void run_case_repeated_span_construction(const std::string& bits, } } -TEST_F(RmMRandomTest, RepeatedSpanConstruction) { +TEST(RmMConstructionTest, RepeatedSpanConstruction) { + std::mt19937_64 rng{kSeed}; std::uniform_int_distribution len_u(0, (int)kMaxBits); for (size_t t = 0; t < kRandomCases; ++t) { const size_t n = (size_t)len_u(rng); @@ -841,7 +847,8 @@ TEST(RmMEdgeCases, SdslStyleParenthesesIndexing) { EXPECT_EQ(rm.enclose(0), pixie::RmMTree::npos); } -TEST_F(RmMRandomTest, LongRandom) { +TYPED_TEST(RmMRandomTest, LongRandom) { + auto& rng = this->rng; std::uniform_int_distribution coin(0, 1); std::uniform_int_distribution len_u(1, (int)kLongMaxBits); std::uniform_int_distribution len_even(0, (int)(kLongMaxBits / 2)); @@ -858,7 +865,7 @@ TEST_F(RmMRandomTest, LongRandom) { } } - run_case_and_compare(bits, rng, kLongOpsPerCase, kSeed); + run_case_and_compare(bits, rng, kLongOpsPerCase, kSeed); } } @@ -1089,13 +1096,13 @@ TEST(RmMBTreeExperimental, OptionalSelectSupport) { auto words = pack_words_lsb_first(bits); pixie::experimental::RmMBTree<> select0_only( std::span(words), bits.size(), - pixie::BitVector::SelectSupport::kSelect0, /*one_count=*/3); + pixie::RankSelectSupport<>::SelectSupport::kSelect0, /*one_count=*/3); pixie::experimental::RmMBTree<> select1_only( std::span(words), bits.size(), - pixie::BitVector::SelectSupport::kSelect1, /*one_count=*/3); + pixie::RankSelectSupport<>::SelectSupport::kSelect1, /*one_count=*/3); pixie::experimental::RmMBTree<> rank_only( std::span(words), bits.size(), - pixie::BitVector::SelectSupport::kNone, /*one_count=*/3); + pixie::RankSelectSupport<>::SelectSupport::kNone, /*one_count=*/3); EXPECT_EQ(select0_only.rank1(bits.size()), 3u); EXPECT_EQ(select0_only.rank0(bits.size()), 3u); diff --git a/src/tests/tree_tests.cpp b/src/tests/tree_tests.cpp new file mode 100644 index 0000000..b828ddc --- /dev/null +++ b/src/tests/tree_tests.cpp @@ -0,0 +1,200 @@ +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace { + +struct LoudsCase { + using Tree = pixie::LoudsTree; + + static std::vector> order( + std::size_t tree_size, + const std::vector>& adjacency) { + return bfs_order(tree_size, adjacency); + } + + static std::vector encode( + std::size_t tree_size, + const std::vector>& adjacency) { + return adj_to_louds(tree_size, adjacency); + } +}; + +struct BpCase { + using Tree = pixie::BPTree; + + static std::vector> order( + std::size_t tree_size, + const std::vector>& adjacency) { + return dfs_order(tree_size, adjacency); + } + + static std::vector encode( + std::size_t tree_size, + const std::vector>& adjacency) { + return pixie::adj_to_bp(tree_size, adjacency); + } +}; + +struct DfudsCase { + using Tree = pixie::DFUDSTree; + + static std::vector> order( + std::size_t tree_size, + const std::vector>& adjacency) { + return dfs_order(tree_size, adjacency); + } + + static std::vector encode( + std::size_t tree_size, + const std::vector>& adjacency) { + return pixie::adj_to_dfuds(tree_size, adjacency); + } +}; + +template +class TreeSpecificationTest : public testing::Test {}; + +using TreeCases = testing::Types; +TYPED_TEST_SUITE(TreeSpecificationTest, TreeCases); + +TYPED_TEST(TreeSpecificationTest, BasicNavigation) { + using Tree = typename TypeParam::Tree; + constexpr std::size_t tree_size = 5; + const std::vector> source = { + {0, 1}, {0, 2}, {1, 3}, {2, 4}, {3}}; + const auto adjacency = TypeParam::order(tree_size, source); + const auto words = TypeParam::encode(tree_size, adjacency); + const Tree tree(words, tree_size); + const AdjListTree reference(adjacency); + + EXPECT_EQ(tree.size(), tree_size); + EXPECT_FALSE(tree.empty()); + auto node = tree.root(); + auto expected = reference.root(); + EXPECT_TRUE(tree.is_root(node)); + for (std::size_t i = 0; i < tree_size; ++i) { + EXPECT_EQ(node.number, expected.number); + EXPECT_EQ(tree.degree(node), reference.degree(expected)); + EXPECT_EQ(tree.is_leaf(node), reference.is_leaf(expected)); + if (tree.is_leaf(node)) { + break; + } + node = tree.child(node, 0); + expected = reference.child(expected, 0); + } +} + +TYPED_TEST(TreeSpecificationTest, RandomTreesSupportDepthFirstNavigation) { + using Tree = typename TypeParam::Tree; + for (std::size_t tree_size = 8; tree_size < (1 << 18); tree_size <<= 1) { + std::mt19937_64 rng(42); + auto adjacency = + TypeParam::order(tree_size, generate_random_tree(tree_size, rng)); + const auto words = TypeParam::encode(tree_size, adjacency); + const Tree tree(words, tree_size); + const AdjListTree reference(adjacency); + + using NodePair = std::pair; + std::stack pending; + pending.push({tree.root(), reference.root()}); + std::size_t visited = 0; + + while (!pending.empty()) { + const auto [node, expected] = pending.top(); + pending.pop(); + ++visited; + EXPECT_EQ(node.number, expected.number); + EXPECT_EQ(tree.parent(node).number, reference.parent(expected).number); + EXPECT_EQ(tree.is_leaf(node), reference.is_leaf(expected)); + EXPECT_EQ(tree.is_root(node), reference.is_root(expected)); + if (!tree.is_root(node)) { + EXPECT_EQ(tree.is_last_child(node), reference.is_last_child(expected)); + if (!tree.is_last_child(node)) { + EXPECT_EQ(tree.next_sibling(node).number, + reference.next_sibling(expected).number); + } + } + + const std::size_t degree = tree.degree(node); + ASSERT_EQ(degree, reference.degree(expected)); + for (std::size_t i = 0; i < degree; ++i) { + pending.push({tree.child(node, i), reference.child(expected, i)}); + } + if (degree != 0) { + EXPECT_EQ(tree.first_child(node).number, + reference.first_child(expected).number); + } + } + EXPECT_EQ(visited, tree_size); + } +} + +TYPED_TEST(TreeSpecificationTest, RandomTreesSupportBreadthFirstNavigation) { + using Tree = typename TypeParam::Tree; + for (std::size_t tree_size = 8; tree_size < (1 << 18); tree_size <<= 1) { + std::mt19937_64 rng(42); + auto adjacency = + TypeParam::order(tree_size, generate_random_tree(tree_size, rng)); + const auto words = TypeParam::encode(tree_size, adjacency); + const Tree tree(words, tree_size); + const AdjListTree reference(adjacency); + + using NodePair = std::pair; + std::queue pending; + pending.push({tree.root(), reference.root()}); + + while (!pending.empty()) { + const auto [node, expected] = pending.front(); + pending.pop(); + const std::size_t degree = tree.degree(node); + ASSERT_EQ(degree, reference.degree(expected)); + for (std::size_t i = 0; i < degree; ++i) { + pending.push({tree.child(node, i), reference.child(expected, i)}); + } + } + } +} + +TYPED_TEST(TreeSpecificationTest, TraversesWithoutAuxiliaryNodeStorage) { + using Tree = typename TypeParam::Tree; + constexpr std::size_t tree_size = 4096; + std::mt19937_64 rng(42); + auto adjacency = + TypeParam::order(tree_size, generate_random_tree(tree_size, rng)); + const auto words = TypeParam::encode(tree_size, adjacency); + const Tree tree(words, tree_size); + + auto node = tree.root(); + bool descending = true; + std::size_t visited = 1; + while (true) { + if (descending && !tree.is_leaf(node)) { + node = tree.first_child(node); + ++visited; + continue; + } + descending = false; + if (tree.is_root(node)) { + break; + } + if (tree.is_last_child(node)) { + node = tree.parent(node); + } else { + node = tree.next_sibling(node); + descending = true; + ++visited; + } + } + EXPECT_EQ(visited, tree_size); +} + +} // namespace diff --git a/src/tests/unittests.cpp b/src/tests/unittests.cpp deleted file mode 100644 index 54b0dd4..0000000 --- a/src/tests/unittests.cpp +++ /dev/null @@ -1,749 +0,0 @@ -#include -#include -#include - -#include -#include -#include - -using pixie::BitVector; -using pixie::BitVectorInterleaved; - -TEST(Rank512, Ones) { - std::array a{std::numeric_limits::max(), - std::numeric_limits::max(), - std::numeric_limits::max(), - std::numeric_limits::max(), - std::numeric_limits::max(), - std::numeric_limits::max(), - std::numeric_limits::max(), - std::numeric_limits::max()}; - for (size_t i = 0; i < 512; ++i) { - auto p = rank_512(a.data(), i); - EXPECT_EQ(p, i); - } -} - -TEST(Rank512, Random) { - std::array a{std::numeric_limits::max(), - std::numeric_limits::max(), - std::numeric_limits::max(), - std::numeric_limits::max(), - std::numeric_limits::max(), - std::numeric_limits::max(), - std::numeric_limits::max(), - std::numeric_limits::max()}; - - std::mt19937_64 rng(42); - for (size_t t = 0; t < 1000; ++t) { - for (size_t i = 0; i < 8; ++i) { - a[i] = rng(); - } - size_t rank = 0; - for (size_t i = 0; i < 512; ++i) { - auto p = rank_512(a.data(), i); - ASSERT_EQ(p, rank); - rank += 1 & (a[i >> 6] >> (i & 63)); - } - auto p = rank_512(a.data(), 512); - ASSERT_EQ(p, rank); - } -} - -TEST(Select64, Ones) { - uint64_t x = std::numeric_limits::max(); - for (size_t i = 0; i < 64; ++i) { - auto p = select_64(x, i); - EXPECT_EQ(p, i); - } -} - -TEST(Select64, Random) { - uint64_t a; - - std::mt19937_64 rng(42); - for (size_t t = 0; t < 1000; ++t) { - a = rng(); - size_t rank = 0; - for (size_t i = 0; i < 64; ++i) { - if (1 & (a >> i)) { - auto p = select_64(a, rank++); - ASSERT_EQ(p, i); - } - } - } -} - -TEST(Select512, Ones) { - std::array a{std::numeric_limits::max(), - std::numeric_limits::max(), - std::numeric_limits::max(), - std::numeric_limits::max(), - std::numeric_limits::max(), - std::numeric_limits::max(), - std::numeric_limits::max(), - std::numeric_limits::max()}; - for (size_t i = 0; i < 512; ++i) { - auto p = select_512(a.data(), i); - EXPECT_EQ(p, i); - } -} - -TEST(SelectZero512, Zeros) { - std::array a{0, 0, 0, 0, 0, 0, 0, 0}; - for (size_t i = 0; i < 512; ++i) { - auto p = select0_512(a.data(), i); - EXPECT_EQ(p, i); - } -} - -TEST(Select512, Random) { - std::array a{std::numeric_limits::max(), - std::numeric_limits::max(), - std::numeric_limits::max(), - std::numeric_limits::max(), - std::numeric_limits::max(), - std::numeric_limits::max(), - std::numeric_limits::max(), - std::numeric_limits::max()}; - - std::mt19937_64 rng(42); - for (size_t t = 0; t < 1000; ++t) { - for (auto& x : a) { - x = rng(); - } - size_t rank = 0; - for (size_t i = 0; i < 512; ++i) { - if (1 & (a[i >> 6] >> (i & 63))) { - auto p = select_512(a.data(), rank++); - ASSERT_EQ(p, i); - } - } - } -} - -TEST(SelectZero512, Random) { - std::array a{std::numeric_limits::max(), - std::numeric_limits::max(), - std::numeric_limits::max(), - std::numeric_limits::max(), - std::numeric_limits::max(), - std::numeric_limits::max(), - std::numeric_limits::max(), - std::numeric_limits::max()}; - - std::mt19937_64 rng(42); - for (size_t t = 0; t < 1000; ++t) { - for (auto& x : a) { - x = rng(); - } - size_t rank = 0; - for (size_t i = 0; i < 512; ++i) { - if ((1 & (a[i >> 6] >> (i & 63))) == 0) { - auto p = select0_512(a.data(), rank++); - ASSERT_EQ(p, i); - } - } - } -} - -TEST(Select512, RankCompativility) { - std::array a{std::numeric_limits::max(), - std::numeric_limits::max(), - std::numeric_limits::max(), - std::numeric_limits::max(), - std::numeric_limits::max(), - std::numeric_limits::max(), - std::numeric_limits::max(), - std::numeric_limits::max()}; - - std::mt19937_64 rng(42); - for (size_t t = 0; t < 1000; ++t) { - for (auto& x : a) { - x = rng(); - } - size_t rank = 0; - for (size_t i = 0; i < 512; ++i) { - if (1 & (a[i >> 6] >> (i & 63))) { - auto p = select_512(a.data(), rank++); - ASSERT_EQ(p, i); - auto r = rank_512(a.data(), p); - ASSERT_EQ(r + 1, rank); - r = rank_512(a.data(), p + 1); - ASSERT_EQ(r, rank); - } - } - } -} - -TEST(BitVectorTest, Basic) { - std::vector bits(8, 0); - bits[0] = 0b101010101; - BitVector bv(bits, 9); - EXPECT_EQ(bv.size(), 9); - EXPECT_EQ(bv[0], 1); - EXPECT_EQ(bv[1], 0); - EXPECT_EQ(bv[2], 1); - EXPECT_EQ(bv[3], 0); - EXPECT_EQ(bv[4], 1); - EXPECT_EQ(bv[5], 0); - EXPECT_EQ(bv[6], 1); - EXPECT_EQ(bv[7], 0); - EXPECT_EQ(bv[8], 1); -} - -TEST(BitVectorTest, MultipleWords) { - std::vector bits(8, 0); - bits[0] = 0xAAAAAAAAAAAAAAAA; - bits[1] = 0x5555555555555555; - BitVector bv(bits, 128); - EXPECT_EQ(bv.size(), 128); - - // First word: 0xAAAAAAAAAAAAAAAA (alternating 1s and 0s, starting with 0) - for (size_t i = 0; i < 64; i++) { - EXPECT_EQ(bv[i], (i % 2 == 1)); - } - - // Second word: 0x5555555555555555 (alternating 0s and 1s, starting with 1) - for (size_t i = 64; i < 128; i++) { - EXPECT_EQ(bv[i], (i % 2 == 0)); - } -} - -TEST(BitVectorTest, ToString) { - std::vector bits(8, 0); - bits[0] = 0b10101010101; - BitVector bv(bits, 11); - EXPECT_EQ(bv.to_string(), "10101010101"); -} - -TEST(BitVectorTest, RankBasic) { - std::vector bits(8, 0); - bits[0] = 0b10110; - BitVector bv(bits, 5); - - EXPECT_EQ(bv.rank(0), 0); // No bits - EXPECT_EQ(bv.rank(1), 0); // 0 - EXPECT_EQ(bv.rank(2), 1); // 10 - EXPECT_EQ(bv.rank(3), 2); // 110 - EXPECT_EQ(bv.rank(4), 2); // 0110 - EXPECT_EQ(bv.rank(5), 3); // 10110 -} - -TEST(BitVectorTest, RankWithZeros) { - std::vector bits(8, 0); - BitVector bv(bits, 5); - - for (size_t i = 0; i <= 5; i++) { - EXPECT_EQ(bv.rank(i), 0); - } -} - -TEST(BitVectorTest, SelectBasic) { - std::vector bits(8, 0); - bits[0] = 0b1100010110010110; - BitVector bv(bits, 16); - - EXPECT_EQ(bv.select(1), 1); - EXPECT_EQ(bv.select(2), 2); - EXPECT_EQ(bv.select(3), 4); - EXPECT_EQ(bv.select(4), 7); - EXPECT_EQ(bv.select(5), 8); - EXPECT_EQ(bv.select(6), 10); - EXPECT_EQ(bv.select(7), 14); - EXPECT_EQ(bv.select(8), 15); -} - -TEST(BitVectorTest, MainRankTest) { - std::mt19937_64 rng(42); - std::vector bits(65536 * 32); - for (size_t i = 0; i < 65536 * 32; i++) { - bits[i] = rng(); - } - - BitVector bv(bits, 65536 * 32 * 64); - uint64_t rank = 0; - for (size_t i = 0; i < bv.size(); ++i) { - ASSERT_EQ(rank, bv.rank(i)); - rank += bv[i]; - } -} - -TEST(BitVectorTest, MainSelectTest) { - std::mt19937_64 rng(42); - std::vector bits(65536 * 32); - for (size_t i = 0; i < 65536 * 32; i++) { - bits[i] = rng(); - } - - BitVector bv(bits, 65536 * 32 * 64); - uint64_t rank = 0; - - for (size_t i = 0; i < bv.size(); ++i) { - if (bv[i]) { - ASSERT_EQ(bv.select(++rank), i); - ASSERT_EQ(bv.rank(i), rank - 1); - ASSERT_EQ(bv.rank(i + 1), rank); - } - } -} - -TEST(BitVectorTest, RankZeroBasic) { - std::vector bits(8, 0); - bits[0] = 0b10110; - BitVector bv(bits, 10); - - EXPECT_EQ(bv.rank0(0), 0); - EXPECT_EQ(bv.rank0(1), 1); - EXPECT_EQ(bv.rank0(2), 1); - EXPECT_EQ(bv.rank0(3), 1); - EXPECT_EQ(bv.rank0(4), 2); - EXPECT_EQ(bv.rank0(5), 2); -} - -TEST(BitVectorTest, RankZeroWithOnes) { - std::vector bits(8, 0); - BitVector bv(bits, 5); - - for (size_t i = 0; i <= 5; i++) { - EXPECT_EQ(bv.rank0(i), i); - } -} - -TEST(BitVectorTest, SelectZeroBasic) { - std::vector bits(8, 0); - bits[0] = 0b1100010110010110; - BitVector bv(bits, 16); - - EXPECT_EQ(bv.select0(1), 0); - EXPECT_EQ(bv.select0(2), 3); - EXPECT_EQ(bv.select0(3), 5); - EXPECT_EQ(bv.select0(4), 6); - EXPECT_EQ(bv.select0(5), 9); - EXPECT_EQ(bv.select0(6), 11); - EXPECT_EQ(bv.select0(7), 12); - EXPECT_EQ(bv.select0(8), 13); -} - -TEST(BitVectorTest, EmptyAndZeroRankGuards) { - BitVector default_bv; - EXPECT_EQ(default_bv.size(), 0u); - EXPECT_EQ(default_bv.rank(0), 0u); - EXPECT_EQ(default_bv.rank0(0), 0u); - EXPECT_EQ(default_bv.select(0), 0u); - EXPECT_EQ(default_bv.select(1), 0u); - EXPECT_EQ(default_bv.select0(0), 0u); - EXPECT_EQ(default_bv.select0(1), 0u); - EXPECT_EQ(default_bv.to_string(), ""); - - std::vector empty; - BitVector bv(std::span(empty), 0); - - EXPECT_EQ(bv.size(), 0u); - EXPECT_EQ(bv.rank(0), 0u); - EXPECT_EQ(bv.rank0(0), 0u); - EXPECT_EQ(bv.select(0), 0u); - EXPECT_EQ(bv.select(1), 0u); - EXPECT_EQ(bv.select0(0), 0u); - EXPECT_EQ(bv.select0(1), 0u); - EXPECT_EQ(bv.to_string(), ""); - - std::vector bits(1, 0b10110); - BitVector non_empty(std::span(bits), 5); - EXPECT_EQ(non_empty.select(0), 0u); - EXPECT_EQ(non_empty.select0(0), 0u); -} - -TEST(BitVectorTest, ExactShortSpanRankSelect) { - std::vector bits = {0b1100010110010110}; - BitVector bv(bits, 16); - - EXPECT_EQ(bv.rank(16), 8); - EXPECT_EQ(bv.rank0(16), 8); - EXPECT_EQ(bv.select(1), 1); - EXPECT_EQ(bv.select(8), 15); - EXPECT_EQ(bv.select(9), bv.size()); - EXPECT_EQ(bv.select0(1), 0); - EXPECT_EQ(bv.select0(8), 13); - EXPECT_EQ(bv.select0(9), bv.size()); -} - -TEST(BitVectorTest, OptionalSelectSupport) { - std::vector bits = {0b1100010110010110}; - - BitVector select0_only(bits, 16, BitVector::SelectSupport::kSelect0); - EXPECT_FALSE(select0_only.supports_select1()); - EXPECT_TRUE(select0_only.supports_select0()); - EXPECT_EQ(select0_only.rank(16), 8); - EXPECT_EQ(select0_only.rank0(16), 8); - EXPECT_EQ(select0_only.select(1), select0_only.size()); - EXPECT_EQ(select0_only.select0(1), 0); - EXPECT_EQ(select0_only.select0(8), 13); - - BitVector hinted_both(bits, 16, BitVector::SelectSupport::kBoth, 8); - EXPECT_TRUE(hinted_both.supports_select1()); - EXPECT_TRUE(hinted_both.supports_select0()); - EXPECT_EQ(hinted_both.select(1), 1); - EXPECT_EQ(hinted_both.select(8), 15); - EXPECT_EQ(hinted_both.select0(1), 0); - EXPECT_EQ(hinted_both.select0(8), 13); - EXPECT_THROW((BitVector(bits, 16, BitVector::SelectSupport::kBoth, 17)), - std::invalid_argument); - - BitVector select1_only(bits, 16, BitVector::SelectSupport::kSelect1); - EXPECT_TRUE(select1_only.supports_select1()); - EXPECT_FALSE(select1_only.supports_select0()); - EXPECT_EQ(select1_only.rank(16), 8); - EXPECT_EQ(select1_only.rank0(16), 8); - EXPECT_EQ(select1_only.select(1), 1); - EXPECT_EQ(select1_only.select(8), 15); - EXPECT_EQ(select1_only.select0(1), select1_only.size()); - - BitVector rank_only(bits, 16, BitVector::SelectSupport::kNone); - EXPECT_FALSE(rank_only.supports_select1()); - EXPECT_FALSE(rank_only.supports_select0()); - EXPECT_EQ(rank_only.rank(16), 8); - EXPECT_EQ(rank_only.rank0(16), 8); - EXPECT_EQ(rank_only.select(0), 0); - EXPECT_EQ(rank_only.select0(0), 0); - EXPECT_EQ(rank_only.select(1), rank_only.size()); - EXPECT_EQ(rank_only.select0(1), rank_only.size()); - - std::vector zero_words(512, 0); - BitVector large_select0_only(zero_words, zero_words.size() * 64, - BitVector::SelectSupport::kSelect0); - EXPECT_EQ(large_select0_only.select(1), large_select0_only.size()); - EXPECT_EQ(large_select0_only.select0(1), 0); - EXPECT_EQ(large_select0_only.select0(16384), 16383); - EXPECT_EQ(large_select0_only.select0(16385), 16384); - EXPECT_EQ(large_select0_only.select0(32768), 32767); - - BitVector large_hinted_select0(zero_words, zero_words.size() * 64, - BitVector::SelectSupport::kSelect0, 0); - EXPECT_EQ(large_hinted_select0.select(1), large_hinted_select0.size()); - EXPECT_EQ(large_hinted_select0.select0(1), 0); - EXPECT_EQ(large_hinted_select0.select0(32768), 32767); - - std::vector one_words(512, ~uint64_t{0}); - BitVector large_select1_only(one_words, one_words.size() * 64, - BitVector::SelectSupport::kSelect1); - EXPECT_EQ(large_select1_only.select0(1), large_select1_only.size()); - EXPECT_EQ(large_select1_only.select(1), 0); - EXPECT_EQ(large_select1_only.select(16384), 16383); - EXPECT_EQ(large_select1_only.select(16385), 16384); - EXPECT_EQ(large_select1_only.select(32768), 32767); -} - -TEST(BitVectorTest, ThrowsWhenOneCountHintUnderestimatesSamples) { - std::vector one_words(512, ~uint64_t{0}); - - EXPECT_THROW((BitVector(one_words, one_words.size() * 64, - BitVector::SelectSupport::kSelect1, 0)), - std::invalid_argument); -} - -TEST(BitVectorTest, ShortSpanUsesScalarRankSelectFallbacks) { - std::vector one_in_second_word = {0, 1}; - BitVector ones(std::span(one_in_second_word), 65); - EXPECT_EQ(ones.rank(64), 0u); - EXPECT_EQ(ones.rank(65), 1u); - EXPECT_EQ(ones.rank0(65), 64u); - EXPECT_EQ(ones.select(1), 64u); - - std::vector zero_in_second_word = { - std::numeric_limits::max(), 0}; - BitVector zeros(std::span(zero_in_second_word), 65); - EXPECT_EQ(zeros.rank(65), 64u); - EXPECT_EQ(zeros.rank0(64), 0u); - EXPECT_EQ(zeros.rank0(65), 1u); - EXPECT_EQ(zeros.select0(1), 64u); -} - -TEST(BitVectorTest, ShortSpanScalarRankUsesPartialWordTail) { - std::vector bits = {~uint64_t{0}, 0, 0b101}; - BitVector bv(std::span(bits), 130); - - EXPECT_EQ(bv.rank(128), 64u); - EXPECT_EQ(bv.rank(129), 65u); - EXPECT_EQ(bv.rank(130), 65u); - EXPECT_EQ(bv.rank0(129), 64u); -} - -TEST(BitVectorTest, IgnoresDirtyPaddingAndTrailingWords) { - std::vector bits = {~uint64_t{0}, ~uint64_t{0}}; - BitVector bv(bits, 3); - - EXPECT_EQ(bv.size(), 3); - EXPECT_EQ(bv.rank(3), 3); - EXPECT_EQ(bv.rank(128), 3); - EXPECT_EQ(bv.rank0(3), 0); - EXPECT_EQ(bv.rank0(128), 0); - EXPECT_EQ(bv.select(1), 0); - EXPECT_EQ(bv.select(3), 2); - EXPECT_EQ(bv.select(4), bv.size()); - EXPECT_EQ(bv.select0(1), bv.size()); -} - -TEST(BitVectorTest, SelectZeroSkewedDistributionFallback) { - constexpr size_t kWordsPerBasicBlock = 8; - constexpr size_t kBasicBlocksPerSuperBlock = 128; - constexpr size_t kZeroBasicBlocks = 40; - - std::vector bits(kWordsPerBasicBlock * kBasicBlocksPerSuperBlock, - ~uint64_t{0}); - std::fill(bits.begin(), bits.begin() + kZeroBasicBlocks * kWordsPerBasicBlock, - 0); - - BitVector bv(std::span(bits), bits.size() * 64); - EXPECT_EQ(bv.rank0(bv.size()), kZeroBasicBlocks * 512u); - EXPECT_EQ(bv.select0(1), 0u); - EXPECT_EQ(bv.select0(10000), 9999u); - EXPECT_EQ(bv.select0(kZeroBasicBlocks * 512u), kZeroBasicBlocks * 512u - 1); - EXPECT_EQ(bv.select0(kZeroBasicBlocks * 512u + 1), bv.size()); -} - -TEST(BitVectorTest, MainRankZeroTest) { - std::mt19937_64 rng(42); - std::vector bits(65536 * 32); - for (size_t i = 0; i < bits.size(); i++) { - bits[i] = rng(); - } - - BitVector bv(bits, bits.size() * 64); - uint64_t zero_count = 0; - for (size_t i = 0; i < bv.size(); ++i) { - ASSERT_EQ(zero_count, bv.rank0(i)); - zero_count += (bv[i] == 0) ? 1 : 0; - } -} - -TEST(BitVectorTest, MainSelectZeroTest) { - std::mt19937_64 rng(42); - std::vector bits(65536 * 32); - for (size_t i = 0; i < bits.size(); i++) { - bits[i] = rng(); - } - - BitVector bv(bits, bits.size() * 64); - uint64_t zero_rank = 0; - - for (size_t i = 0; i < bv.size(); ++i) { - if (bv[i] == 0) { - zero_rank++; - EXPECT_EQ(bv.select0(zero_rank), i); - EXPECT_EQ(bv.rank0(i), zero_rank - 1); - EXPECT_EQ(bv.rank0(i + 1), zero_rank); - } - } -} - -TEST(BitVectorInterleavedTest, AtTest) { - std::mt19937_64 rng(42); - /* - * TODO: need to check edge cases, at least - * non-multiple of 64 size - */ - std::vector bits(65536 * 32); - for (size_t i = 0; i < 65536 * 32; i++) { - bits[i] = rng(); - } - - BitVectorInterleaved bv(bits, 65536 * 32 * 64); - uint64_t rank = 0; - for (size_t i = 0; i < 65536 * 32; ++i) { - for (size_t j = 0; j < 64; ++j) { - ASSERT_EQ((bits[i] >> j) & 1, bv[i * 64 + j]); - } - } -} - -TEST(BitVectorInterleavedTest, MainTest) { - std::mt19937_64 rng(42); - std::vector bits(65536 * 32); - for (size_t i = 0; i < 65536 * 32; i++) { - bits[i] = rng(); - } - - BitVectorInterleaved bv(bits, 65536 * 32 * 64); - uint64_t rank = 0; - for (size_t i = 0; i < bv.size(); ++i) { - ASSERT_EQ(rank, bv.rank(i)); - rank += bv[i]; - } -} - -TEST(LowerBound4x64, Random) { - std::vector x(8); - std::mt19937_64 rng(42); - for (size_t i = 0; i < 1000; i++) { - uint64_t y = rng(); - uint16_t cnt = 0; - bool fl = 1; - for (size_t j = 0; j < 4; j++) { - x[j] = rng(); - fl &= x[j] < y; - cnt += fl; - } - if (cnt < 4) { - ASSERT_EQ(lower_bound_4x64(x.data(), y), cnt); - } else { - ASSERT_GE(lower_bound_4x64(x.data(), y), cnt); - } - } -} - -TEST(LowerBound8x64, Random) { - std::vector x(8); - std::mt19937_64 rng(42); - for (size_t i = 0; i < 1000; i++) { - uint64_t y = rng(); - uint16_t cnt = 0; - bool fl = 1; - for (size_t j = 0; j < 8; j++) { - x[j] = rng(); - fl &= x[j] < y; - cnt += fl; - } - if (cnt < 8) { - ASSERT_EQ(lower_bound_8x64(x.data(), y), cnt); - } else { - ASSERT_GE(lower_bound_8x64(x.data(), y), cnt); - } - } -} - -TEST(LowerBoundDelta4x64, Random) { - std::vector x(4); - uint64_t dlt_array[4]; - std::mt19937_64 rng(42); - for (size_t i = 0; i < 100000; i++) { - uint64_t y = rng(); - uint64_t dlt_scalar = rng(); - uint16_t cnt = 0; - bool fl = 1; - for (size_t j = 0; j < 4; j++) { - dlt_array[j] = rng(); - x[j] = rng(); - fl &= dlt_scalar + dlt_array[j] - x[j] < y; - cnt += fl; - } - if (cnt < 4) { - ASSERT_EQ(lower_bound_delta_4x64(x.data(), y, dlt_array, dlt_scalar), - cnt); - } else { - ASSERT_GE(lower_bound_delta_4x64(x.data(), y, dlt_array, dlt_scalar), - cnt); - } - } -} - -TEST(LowerBoundDelta8x64, Random) { - std::vector x(8); - uint64_t dlt_array[8]; - std::mt19937_64 rng(42); - for (size_t i = 0; i < 100000; i++) { - uint64_t y = rng(); - uint64_t dlt_scalar = rng(); - uint16_t cnt = 0; - bool fl = 1; - for (size_t j = 0; j < 8; j++) { - dlt_array[j] = rng(); - x[j] = rng(); - fl &= dlt_scalar + dlt_array[j] - x[j] < y; - cnt += fl; - } - if (cnt < 8) { - ASSERT_EQ(lower_bound_delta_8x64(x.data(), y, dlt_array, dlt_scalar), - cnt); - } else { - ASSERT_GE(lower_bound_delta_8x64(x.data(), y, dlt_array, dlt_scalar), - cnt); - } - } -} - -TEST(LowerBound32x16, Random) { - std::vector x(32); - std::mt19937 rng(42); - for (size_t i = 0; i < 1000; i++) { - uint16_t y = rng(); - uint16_t cnt = 0; - for (size_t j = 0; j < 32; j++) { - x[j] = rng(); - cnt += x[j] < y; - } - ASSERT_EQ(lower_bound_32x16(x.data(), y), cnt); - } -} - -TEST(LowerBoundDelta32x16, Random) { - std::vector x(32); - uint16_t dlt_array[32]; - std::mt19937 rng(42); - for (size_t i = 0; i < 100000; i++) { - uint16_t y = rng(); - uint16_t dlt_scalar = rng(); - uint16_t cnt = 0; - for (size_t j = 0; j < 32; j++) { - x[j] = rng(); - if (dlt_scalar < x[j]) { - dlt_array[j] = - x[j] - dlt_scalar + rng() % ((1 << 16) - x[j] + dlt_scalar); - } else { - dlt_array[j] = rng() % ((1 << 16) - dlt_scalar); - } - cnt += dlt_array[j] + dlt_scalar - x[j] < y; - } - ASSERT_EQ(lower_bound_delta_32x16(x.data(), y, dlt_array, dlt_scalar), cnt); - } -} - -TEST(Popcount64x4, Random) { - std::vector x(32); - std::vector y(32); - std::mt19937 rng(42); - for (size_t i = 0; i < 1000; i++) { - for (size_t j = 0; j < 32; j++) { - x[j] = rng(); - } - popcount_64x4(x.data(), y.data()); - for (size_t j = 0; j < 32; ++j) { - uint8_t a = x[j] & 0x0F; - ASSERT_EQ(y[j] & 0x0F, std::popcount(a)); - a = x[j] >> 4; - ASSERT_EQ(y[j] >> 4, std::popcount(a)); - } - } -} - -TEST(Popcount32x8, Random) { - std::vector x(32); - std::vector y(32); - std::mt19937 rng(42); - for (size_t i = 0; i < 1000; i++) { - for (size_t j = 0; j < 32; j++) { - x[j] = rng(); - } - popcount_32x8(x.data(), y.data()); - for (size_t j = 0; j < 32; ++j) { - ASSERT_EQ(y[j], std::popcount(x[j])); - } - } -} - -TEST(RankParallel32x8, Random) { - std::vector x(32); - std::vector sum(32); - std::vector y(32); - std::mt19937 rng(42); - for (size_t i = 0; i < 1000; i++) { - x[0] = rng(); - sum[0] = std::popcount(x[0]); - for (size_t j = 1; j < 32; j++) { - x[j] = rng(); - sum[j] = sum[j - 1] + std::popcount(x[j]); - } - rank_32x8(x.data(), y.data()); - ASSERT_EQ(std::equal(y.begin(), y.end(), sum.begin()), true); - } -} diff --git a/src/tests/wavelet_tree_tests.cpp b/src/tests/wavelet_tree_tests.cpp index e8a0fa1..03e900e 100644 --- a/src/tests/wavelet_tree_tests.cpp +++ b/src/tests/wavelet_tree_tests.cpp @@ -1,6 +1,6 @@ #include #include -#include +#include #include @@ -164,12 +164,11 @@ TEST(WaveletTreeTest, SerializationSmoke) { reinterpret_cast(serialized_data.data()), serialized_data.size() * sizeof(uint64_t)); - auto mmap_tree = - pixie::WaveletTreeBase::deserialize(byte_span); + auto view_tree = pixie::WaveletTreeView::deserialize(byte_span); for (size_t i = 0; i <= data_size; i += 16) { uint64_t symb = data[i == data_size ? 0 : i]; - EXPECT_EQ(orig_tree.rank(symb, i), mmap_tree.rank(symb, i)); + EXPECT_EQ(orig_tree.rank(symb, i), view_tree.rank(symb, i)); } std::vector count(alphabet_size, 0); @@ -179,12 +178,12 @@ TEST(WaveletTreeTest, SerializationSmoke) { for (uint64_t symb = 0; symb < alphabet_size; symb++) { for (uint64_t rank = 1; rank <= count[symb]; rank++) { - EXPECT_EQ(orig_tree.select(symb, rank), mmap_tree.select(symb, rank)); + EXPECT_EQ(orig_tree.select(symb, rank), view_tree.select(symb, rank)); } } auto orig_segment = orig_tree.get_segment(0, data_size); - auto mmap_segment = mmap_tree.get_segment(0, data_size); - EXPECT_EQ(orig_segment, mmap_segment); + auto view_segment = view_tree.get_segment(0, data_size); + EXPECT_EQ(orig_segment, view_segment); } }