Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 17 additions & 13 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ space usage close to the theoretical minimum.
Current library families are:

- rank/select support over packed bit sequences;
- positional and monotone packed integer vectors;
- range min-max (RmM) indexes;
- static range-minimum-query (RMQ) indexes;
- rooted-tree encodings (LOUDS, balanced parentheses, and DFUDS);
Expand Down Expand Up @@ -48,10 +49,11 @@ adds Pixie context; it does not replace the shared guidance.
- **`include/pixie/<family>.h`**: Lightweight CRTP contract for one library
family, such as `rank_select.h`, `rmq.h`, or `storage.h`.
- **`include/pixie/<family>/`**: Concrete implementations for that family.
- **`include/pixie/<family>/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/<family>/implementations.h`**: Reference catalog for family
benchmark translation units. It includes the contract and every concrete
implementation and retains current benchmark snapshots where applicable.
Tests and public consumers include specific headers; 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.
Expand All @@ -70,8 +72,8 @@ adds Pixie context; it does not replace the shared guidance.
### Family Interface Pattern

Public data-structure families use CRTP contracts. The current contracts are
`RankSelectBase`, `RmMBase`, `pixie::rmq::RmqBase`, `TreeBase`, `StorageBase`,
and `WaveletTreeBase`.
`IntegerVectorBase`, `MonotoneIntegerVectorBase`, `RankSelectBase`, `RmMBase`,
`pixie::rmq::RmqBase`, `TreeBase`, `StorageBase`, and `WaveletTreeBase`.

1. Define or extend the public contract in `include/pixie/<family>.h`.
Public facade methods delegate to a clearly named `*_impl()` method on the
Expand All @@ -80,8 +82,9 @@ and `WaveletTreeBase`.
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.
4. Include concrete headers directly in the family test harness and include the
catalog in the benchmark harness. 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
Expand Down Expand Up @@ -244,9 +247,9 @@ 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`,
`serialization_tests`, `excess_positions_tests`, `excess_record_lows_tests`,
and `rmq_tests`. Run an executable directly only when debugging a focused
Google Test filter.
`serialization_tests`, `integer_vector_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

Expand Down Expand Up @@ -312,8 +315,9 @@ The script configures and builds the `coverage` preset, deletes stale
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
6. Keep public contracts lightweight. Only benchmark translation units include
family catalogs; tests and public consumers include specific contract or
concrete headers. Do not add compatibility forwarding headers unless the
user explicitly requests a compatibility layer.

## CI/CD Workflows
Expand Down
19 changes: 19 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,15 @@ if (PIXIE_TESTS)
gtest_main
${PIXIE_DIAGNOSTICS_LIBS})

add_executable(integer_vector_tests
src/tests/integer_vector_tests.cpp)
target_include_directories(integer_vector_tests
PUBLIC include)
target_link_libraries(integer_vector_tests
gtest
gtest_main
${PIXIE_DIAGNOSTICS_LIBS})

add_executable(excess_positions_tests
src/tests/excess_positions_tests.cpp)
target_include_directories(excess_positions_tests
Expand Down Expand Up @@ -311,6 +320,7 @@ if (PIXIE_TESTS)
file_archive_tests
storage_tests
serialization_tests
integer_vector_tests
excess_positions_tests
select512_experimental_tests
excess_record_lows_tests
Expand Down Expand Up @@ -422,6 +432,14 @@ if (PIXIE_BENCHMARKS)
benchmark_main
${PIXIE_DIAGNOSTICS_LIBS})

add_executable(integer_vector_benchmarks
src/benchmarks/integer_vector_benchmarks.cpp)
target_include_directories(integer_vector_benchmarks
PUBLIC include)
target_link_libraries(integer_vector_benchmarks
benchmark
${PIXIE_DIAGNOSTICS_LIBS})

add_executable(bp_tree_benchmarks
src/benchmarks/bp_tree_benchmarks.cpp)
target_include_directories(bp_tree_benchmarks
Expand Down Expand Up @@ -483,6 +501,7 @@ if (PIXIE_BENCHMARKS)
louds_tree_benchmarks
wavelet_tree_benchmarks
file_archive_benchmarks
integer_vector_benchmarks
serialization_benchmarks
bp_tree_benchmarks
dfuds_tree_benchmarks
Expand Down
33 changes: 21 additions & 12 deletions include/pixie/bits.h
Original file line number Diff line number Diff line change
@@ -1,15 +1,5 @@
#pragma once

#include <immintrin.h>

#include <algorithm>
#include <array>
#include <bit>
#include <cstddef>
#include <cstdint>
#include <limits>
#include <numeric>

#if defined(__AVX512VPOPCNTDQ__) && defined(__AVX512F__) && \
defined(__AVX512BW__)
#define PIXIE_AVX512_SUPPORT
Expand All @@ -21,6 +11,26 @@

#ifdef __AVX2__
#define PIXIE_AVX2_SUPPORT
#endif

#if defined(__SSSE3__) && defined(__SSE4_1__)
#define PIXIE_SSE41_SUPPORT
#endif

#if defined(PIXIE_AVX512_SUPPORT) || defined(PIXIE_BMI2_SUPPORT) || \
defined(PIXIE_AVX2_SUPPORT) || defined(PIXIE_SSE41_SUPPORT)
#include <immintrin.h>
#endif

#include <algorithm>
#include <array>
#include <bit>
#include <cstddef>
#include <cstdint>
#include <limits>
#include <numeric>

#ifdef PIXIE_AVX2_SUPPORT
// Lookup table for 4-bit popcount
// This table maps each 4-bit value (0-15) to its population count
// clang-format off
Expand Down Expand Up @@ -54,8 +64,7 @@ static inline const __m256i mask_first_half = _mm256_setr_epi8(
static inline constexpr int8_t excess_nibble_min_offset[16] = {
4, 4, 4, 4, 2, 2, 1, 1, 3, 3, 1, 1, 2, 2, 1, 1};

#if defined(__SSSE3__) && defined(__SSE4_1__)
#define PIXIE_SSE41_SUPPORT
#ifdef PIXIE_SSE41_SUPPORT
// clang-format off
static inline const __m128i excess_lut_delta_sse = _mm_setr_epi8(
-4, -2, -2, 0,
Expand Down
2 changes: 1 addition & 1 deletion include/pixie/file_archive.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

#include <pixie/detail/serialization.h>
#include <pixie/storage/read_only_view.h>
#include <pixie/wavelet_tree/implementations.h>
#include <pixie/wavelet_tree/index.h>

#include <algorithm>
#include <array>
Expand Down
158 changes: 158 additions & 0 deletions include/pixie/integer_vector.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
#pragma once

/**
* @file integer_vector.h
* @brief Common interfaces for immutable positional integer vectors.
*
* Include a concrete header under `<pixie/integer_vector/>` to use an
* integer-vector implementation.
*/

#include <concepts>
#include <cstddef>
#include <cstdint>
#include <limits>
#include <span>
#include <stdexcept>
#include <type_traits>

namespace pixie {

/** @brief An unsigned integer-vector value type no wider than 64 bits. */
template <class Value>
concept IntegerVectorValue = std::unsigned_integral<Value> &&
std::same_as<Value, std::remove_cv_t<Value>> &&
!std::same_as<std::remove_cv_t<Value>, bool> &&
(std::numeric_limits<Value>::digits <= 64);

/**
* @brief CRTP facade for an immutable positional integer vector.
*
* @tparam Impl Concrete implementation providing `size_impl()`,
* `value_at_impl()`, and `copy_to_impl()`. It may also provide
* `memory_usage_bytes_impl()`.
* @tparam Value Unsigned element type other than `bool`, no wider than 64 bits.
*
* @details Positions are zero-based. Implementations retain ownership and
* lifetime semantics documented by their concrete types.
*/
template <class Impl, IntegerVectorValue Value = std::uint64_t>
class IntegerVectorBase {
public:
/** @brief Stored integer type. */
using value_type = Value;

/** @brief Type used for element counts and zero-based positions. */
using size_type = std::size_t;

/** @brief Return the logical number of elements. */
size_type size() const { return impl().size_impl(); }

/** @brief Return whether the vector has no elements. */
bool empty() const { return size() == 0; }

/**
* @brief Read the element at zero-based @p position without bounds checking.
* @param position Position in `[0, size())`.
* @return The stored value.
* @pre `position < size()`.
*/
value_type operator[](size_type position) const {
return impl().value_at_impl(position);
}

/**
* @brief Read the element at zero-based @p position with bounds checking.
* @throws std::out_of_range if `position >= size()`.
*/
value_type at(size_type position) const {
if (position >= size()) {
throw std::out_of_range("Integer-vector position is out of range");
}
return (*this)[position];
}

/**
* @brief Copy a checked contiguous source range into @p output.
*
* @details Copies `[begin, begin + output.size())`. The complete source
* range is validated before the implementation is called, so an invalid
* range leaves @p output unchanged.
*
* @throws std::out_of_range if the requested source range is invalid.
*/
void copy_to(size_type begin, std::span<value_type> output) const {
if (begin > size() || output.size() > size() - begin) {
throw std::out_of_range("Integer-vector copy range is out of bounds");
}
impl().copy_to_impl(begin, output);
}

/**
* @brief Return total memory owned by this vector when supported.
* @return Inline object bytes plus storage owned below the object. Borrowed
* backing bytes are excluded.
*/
size_type memory_usage_bytes() const
requires requires(const Impl& concrete) {
{ concrete.memory_usage_bytes_impl() } -> std::convertible_to<size_type>;
}
{
return impl().memory_usage_bytes_impl();
}

private:
/** @brief Return this facade as its concrete CRTP implementation. */
const Impl& impl() const { return static_cast<const Impl&>(*this); }
};

/**
* @brief CRTP refinement for duplicate-preserving monotone integer vectors.
*
* @tparam Impl Concrete implementation providing the integer-vector extension
* points plus `lower_bound_index_impl()` and `upper_bound_index_impl()`.
* @tparam Value Unsigned element type other than `bool`, no wider than 64 bits.
*
* @details Monotone means nondecreasing; equal adjacent values are retained.
* This contract does not prescribe representation-independent query
* complexity.
*/
template <class Impl, IntegerVectorValue Value = std::uint64_t>
class MonotoneIntegerVectorBase : public IntegerVectorBase<Impl, Value> {
public:
using typename IntegerVectorBase<Impl, Value>::size_type;
using typename IntegerVectorBase<Impl, Value>::value_type;

/**
* @brief Return the first index whose value is greater than or equal to @p x.
* @return An index in `[0, size()]`; `size()` means no such element exists.
*/
size_type lower_bound_index(value_type x) const {
return impl().lower_bound_index_impl(x);
}

/**
* @brief Return the first index whose value is greater than @p x.
* @return An index in `[0, size()]`; `size()` means no such element exists.
*/
size_type upper_bound_index(value_type x) const {
return impl().upper_bound_index_impl(x);
}

/** @brief Return whether at least one element equals @p x. */
bool contains(value_type x) const {
const size_type position = lower_bound_index(x);
return position != this->size() && (*this)[position] == x;
}

/** @brief Return the number of elements equal to @p x. */
size_type count(value_type x) const {
return upper_bound_index(x) - lower_bound_index(x);
}

private:
/** @brief Return this facade as its concrete CRTP implementation. */
const Impl& impl() const { return static_cast<const Impl&>(*this); }
};

} // namespace pixie
50 changes: 50 additions & 0 deletions include/pixie/integer_vector/implementations.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
#pragma once

/**
* @file implementations.h
* @brief All integer-vector implementations provided by Pixie.
*
* - `PackedIntegerVector`: owning runtime-width packed integers.
* - `PackedIntegerVectorView`: zero-copy packed-integer view.
* - `PackedMonotoneIntegerVector`: owning validated monotone packed integers.
* - `PackedMonotoneIntegerVectorView`: zero-copy monotone packed-integer view.
*/

// clang-format off
/*
* Packed monotone integer-vector benchmark snapshot, 2026-08-25.
*
* The table reports one pinned Release pass on CPU 0 of an AMD Ryzen 7 8845HS,
* with Google Benchmark's 0.1 s warmup and 0.5 s minimum time. Construction,
* serialization, view restoration, and the deterministic 2^16-query pool are
* outside the timed region. Times are CPU nanoseconds per lower-bound query,
* rounded to the nearest nanosecond.
*
* Dense values are `i`; duplicate-heavy values are `i / 16`; sparse values
* are deterministic cumulative increments in `[1, 1024]` (seed 42). Queries
* alternate between sampled present values and their successor.
*
* | dataset | N | owner | view |
* | :-------------- | ---: | ----: | ---: |
* | dense | 2^10 | 67 | 67 |
* | dense | 2^14 | 90 | 91 |
* | dense | 2^18 | 142 | 142 |
* | dense | 2^22 | 515 | 566 |
* | dense | 2^26 | 1546 | 1659 |
* | duplicate-heavy | 2^10 | 45 | 43 |
* | duplicate-heavy | 2^14 | 68 | 70 |
* | duplicate-heavy | 2^18 | 118 | 122 |
* | duplicate-heavy | 2^22 | 446 | 486 |
* | duplicate-heavy | 2^26 | 1517 | 1546 |
* | sparse | 2^10 | 60 | 68 |
* | sparse | 2^14 | 94 | 93 |
* | sparse | 2^18 | 151 | 170 |
* | sparse | 2^22 | 746 | 884 |
* | sparse | 2^26 | 1956 | 1907 |
*/
// clang-format on

#include <pixie/integer_vector.h>
#include <pixie/integer_vector/monotone.h>
#include <pixie/integer_vector/packed.h>
#include <pixie/integer_vector/packed_monotone.h>
Loading
Loading