diff --git a/AGENTS.md b/AGENTS.md index 79181fc..d91f533 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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); @@ -48,10 +49,11 @@ adds Pixie context; it does not replace the shared guidance. - **`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//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. @@ -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/.h`. Public facade methods delegate to a clearly named `*_impl()` method on the @@ -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 @@ -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 @@ -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 diff --git a/CMakeLists.txt b/CMakeLists.txt index fff9bf9..f6aa200 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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 @@ -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 @@ -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 @@ -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 diff --git a/include/pixie/bits.h b/include/pixie/bits.h index 6ecc980..ed03503 100644 --- a/include/pixie/bits.h +++ b/include/pixie/bits.h @@ -1,15 +1,5 @@ #pragma once -#include - -#include -#include -#include -#include -#include -#include -#include - #if defined(__AVX512VPOPCNTDQ__) && defined(__AVX512F__) && \ defined(__AVX512BW__) #define PIXIE_AVX512_SUPPORT @@ -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 +#endif + +#include +#include +#include +#include +#include +#include +#include + +#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 @@ -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, diff --git a/include/pixie/file_archive.h b/include/pixie/file_archive.h index 7e0f61a..fa7de6e 100644 --- a/include/pixie/file_archive.h +++ b/include/pixie/file_archive.h @@ -7,7 +7,7 @@ #include #include -#include +#include #include #include diff --git a/include/pixie/integer_vector.h b/include/pixie/integer_vector.h new file mode 100644 index 0000000..33337b3 --- /dev/null +++ b/include/pixie/integer_vector.h @@ -0,0 +1,158 @@ +#pragma once + +/** + * @file integer_vector.h + * @brief Common interfaces for immutable positional integer vectors. + * + * Include a concrete header under `` to use an + * integer-vector implementation. + */ + +#include +#include +#include +#include +#include +#include +#include + +namespace pixie { + +/** @brief An unsigned integer-vector value type no wider than 64 bits. */ +template +concept IntegerVectorValue = std::unsigned_integral && + std::same_as> && + !std::same_as, bool> && + (std::numeric_limits::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 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 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; + } + { + return impl().memory_usage_bytes_impl(); + } + + private: + /** @brief Return this facade as its concrete CRTP implementation. */ + const Impl& impl() const { return static_cast(*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 MonotoneIntegerVectorBase : public IntegerVectorBase { + public: + using typename IntegerVectorBase::size_type; + using typename IntegerVectorBase::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(*this); } +}; + +} // namespace pixie diff --git a/include/pixie/integer_vector/implementations.h b/include/pixie/integer_vector/implementations.h new file mode 100644 index 0000000..4d09ddd --- /dev/null +++ b/include/pixie/integer_vector/implementations.h @@ -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 +#include +#include +#include diff --git a/include/pixie/integer_vector/monotone.h b/include/pixie/integer_vector/monotone.h new file mode 100644 index 0000000..fc9c18a --- /dev/null +++ b/include/pixie/integer_vector/monotone.h @@ -0,0 +1,196 @@ +#pragma once + +/** + * @file monotone.h + * @brief Validated monotone adapters for immutable integer vectors. + */ + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace pixie { + +/** + * @brief Owning duplicate-preserving monotone adapter over an integer vector. + * + * @tparam Vector Immutable integer-vector implementation owned by value. + * + * @details Public construction scans once and rejects decreasing adjacent + * values. The underlying vector remains private so the nondecreasing invariant + * cannot be invalidated. Bounds use logarithmic index-based binary search for + * this adapter. + */ +template + requires requires { typename Vector::value_type; } && + IntegerVectorValue && + std::derived_from< + Vector, + IntegerVectorBase> +class MonotoneIntegerVector + : public MonotoneIntegerVectorBase, + typename Vector::value_type>, + public SerializationBase> { + public: + /** @brief Stored unsigned integer type inherited from the nested vector. */ + using value_type = typename Vector::value_type; + + /** @brief Type used for element counts and positions. */ + using size_type = std::size_t; + + /** + * @brief Take ownership of @p vector after validating nondecreasing order. + * @throws std::invalid_argument if any value is smaller than its predecessor. + */ + explicit MonotoneIntegerVector(Vector vector) : vector_(std::move(vector)) { + validate_monotone(); + } + + /** @brief Return the number of retained values. */ + size_type size_impl() const { return vector_.size(); } + + /** @brief Read one valid zero-based position without bounds checking. */ + value_type value_at_impl(size_type position) const { + return vector_[position]; + } + + /** @brief Copy a range already validated by the public facade. */ + void copy_to_impl(size_type begin, std::span output) const { + vector_.copy_to(begin, output); + } + + /** @brief Return inline bytes plus memory owned below the nested vector. */ + size_type memory_usage_bytes_impl() const + requires requires(const Vector& vector) { + { vector.memory_usage_bytes() } -> std::convertible_to; + } + { + return sizeof(*this) + nested_owned_memory_bytes(vector_); + } + + /** @brief Return the first index whose value is at least @p x. */ + size_type lower_bound_index_impl(value_type x) const { + size_type first = 0; + size_type count = vector_.size(); + while (count != 0) { + const size_type step = count / 2; + const size_type middle = first + step; + if (vector_[middle] < x) { + first = middle + 1; + count -= step + 1; + } else { + count = step; + } + } + return first; + } + + /** @brief Return the first index whose value is greater than @p x. */ + size_type upper_bound_index_impl(value_type x) const { + size_type first = 0; + size_type count = vector_.size(); + while (count != 0) { + const size_type step = count / 2; + const size_type middle = first + step; + if (x < vector_[middle]) { + count = step; + } else { + first = middle + 1; + count -= step + 1; + } + } + return first; + } + + /** + * @brief Write a version-1 monotone wrapper around one nested vector + * artifact. + * @throws std::invalid_argument if the wrapper would not start at an + * eight-byte-aligned writer offset. + */ + void serialize_impl(BinaryWriter& writer) const + requires Serializable + { + if (writer.size_bytes() % alignof(std::uint64_t) != 0) { + throw std::invalid_argument( + "Monotone integer-vector serialization requires an aligned offset"); + } + const std::size_t artifact_begin = writer.size_bytes(); + detail::write_magic(writer, kSerializationMagic); + writer.write_u32(kSerializationVersion); + writer.write_u32(0); + const std::size_t size_position = writer.write_u64_placeholder(); + vector_.serialize(writer); + writer.patch_u64(size_position, static_cast( + writer.size_bytes() - artifact_begin)); + } + + /** + * @brief Restore one framed monotone vector and advance @p reader on success. + * + * @details Quick validation checks both frames but trusts the encoded + * ordering. Full validation also scans all decoded values and rejects a + * decrease. The nested vector determines whether restored storage is owned + * or borrowed. Failure leaves @p reader unchanged. + */ + static MonotoneIntegerVector deserialize_impl( + BinaryReader& reader, + DeserializationValidation validation = DeserializationValidation::kQuick) + requires Deserializable + { + BinaryReader candidate = reader; + const std::size_t available_size = candidate.remaining(); + detail::require_magic(candidate, kSerializationMagic); + if (candidate.read_u32() != kSerializationVersion || + candidate.read_u32() != 0) { + throw std::invalid_argument( + "Incompatible serialized monotone integer vector"); + } + const std::size_t artifact_size = detail::checked_artifact_size( + candidate.read_u64(), kSerializationHeaderBytes, available_size); + BinaryReader payload = + candidate.read_subreader(artifact_size - kSerializationHeaderBytes); + Vector vector = Vector::deserialize(payload, validation); + payload.require_zero_padding(0); + + MonotoneIntegerVector result(std::move(vector), TrustedTag{}); + if (validation == DeserializationValidation::kFull) { + result.validate_monotone(); + } + reader = candidate; + return result; + } + + private: + struct TrustedTag {}; + + inline static constexpr std::array kSerializationMagic = { + 'P', 'X', 'M', 'O', 'N', 'O', 'V', '1'}; + static constexpr std::uint32_t kSerializationVersion = 1; + static constexpr std::size_t kSerializationHeaderBytes = 24; + + MonotoneIntegerVector(Vector vector, TrustedTag) + : vector_(std::move(vector)) {} + + void validate_monotone() const { + for (size_type i = 1; i < vector_.size(); ++i) { + if (vector_[i] < vector_[i - 1]) { + throw std::invalid_argument( + "Monotone integer vector contains a decreasing pair"); + } + } + } + + Vector vector_; +}; + +} // namespace pixie diff --git a/include/pixie/integer_vector/packed.h b/include/pixie/integer_vector/packed.h new file mode 100644 index 0000000..edbe7d8 --- /dev/null +++ b/include/pixie/integer_vector/packed.h @@ -0,0 +1,406 @@ +#pragma once + +/** + * @file packed.h + * @brief Runtime-width packed immutable integer vectors. + */ + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace pixie { + +/** + * @brief Immutable runtime-width packed integer vector over configurable + * storage. + * + * @tparam Storage `AlignedStorage` for an owner or `ReadOnlyStorageView` for a + * zero-copy view. + * @tparam Value Unsigned element type other than `bool`, no wider than 64 bits. + * + * @details Values are packed LSB-first into 64-bit words. An owning vector + * stores cache-line-padded allocation internally, but only logical packed + * words are serialized. A view borrows its serialized payload; the backing + * bytes must remain alive, immutable, and aligned for the view's lifetime. + */ +template + requires(std::same_as || + std::same_as) +class BasicPackedIntegerVector + : public IntegerVectorBase, Value>, + public SerializationBase> { + public: + /** @brief Stored unsigned integer type. */ + using value_type = Value; + + /** @brief Type used for element counts, widths, and positions. */ + using size_type = std::size_t; + + /** @brief Construct an empty width-zero vector. */ + BasicPackedIntegerVector() = default; + + /** + * @brief Pack @p values using their minimum required width. + * @details Empty and all-zero inputs use width zero and no payload. + * @throws std::length_error if the packed dimensions overflow `size_t`. + */ + explicit BasicPackedIntegerVector(std::span values) + requires std::same_as + : BasicPackedIntegerVector(values, + inferred_width(values), + InferredTag{}) {} + + /** + * @brief Pack @p values after validating an explicit maximum @p width. + * + * @details Width must be in `[0, digits(value_type)]`. Every value must fit; + * values are never truncated. Empty and all-zero inputs are normalized to + * canonical width zero. + * + * @throws std::invalid_argument if the width is invalid or a value does not + * fit. + * @throws std::length_error if the packed dimensions overflow `size_t`. + */ + BasicPackedIntegerVector(std::span values, size_type width) + requires std::same_as + : BasicPackedIntegerVector(values, width, ExplicitTag{}) {} + + /** @brief Return the logical runtime width in bits, in `[0, digits(Value)]`. + */ + size_type width() const { return width_; } + + /** @brief Return the logical number of elements. */ + size_type size_impl() const { return size_; } + + /** @brief Read one valid zero-based position without bounds checking. */ + value_type value_at_impl(size_type position) const { + return read_field(logical_words(), position, width_); + } + + /** @brief Copy a range already validated by the public facade. */ + void copy_to_impl(size_type begin, std::span output) const { + for (size_type i = 0; i < output.size(); ++i) { + output[i] = value_at_impl(begin + i); + } + } + + /** + * @brief Return inline bytes plus owned storage capacity. + * @details A read-only view excludes all borrowed backing bytes. + */ + size_type memory_usage_bytes_impl() const { + if constexpr (std::same_as) { + return sizeof(*this) + storage_.allocated_bytes(); + } else { + return sizeof(*this); + } + } + + /** + * @brief Write a version-1 canonical little-endian packed-vector artifact. + * + * @details Exactly `ceil(size() * width() / 64)` logical words are written; + * owner allocation padding is excluded. The artifact can later be restored + * as either an independent owner or a zero-copy read-only view. + * + * @throws std::invalid_argument if the artifact would not begin at an + * eight-byte-aligned writer offset. + */ + void serialize_impl(BinaryWriter& writer) const { + if (writer.size_bytes() % alignof(std::uint64_t) != 0) { + throw std::invalid_argument( + "Packed integer-vector serialization requires an aligned offset"); + } + + const Dimensions dimensions = checked_dimensions(size_, width_); + const std::size_t artifact_begin = writer.size_bytes(); + detail::write_magic(writer, kSerializationMagic); + writer.write_u32(kSerializationVersion); + writer.write_u8(detail::kLittleEndianMarker); + writer.write_u8(static_cast(kValueDigits)); + writer.write_u8(static_cast(width_)); + writer.write_u8(0); + const std::size_t size_position = writer.write_u64_placeholder(); + writer.write_size(size_); + + const auto words = logical_words(); + for (std::size_t i = 0; i < dimensions.word_count; ++i) { + std::uint64_t word = words[i]; + if (i + 1 == dimensions.word_count && dimensions.bit_count % 64 != 0) { + word &= low_bits_mask(dimensions.bit_count % 64); + } + writer.write_u64(word); + } + writer.patch_u64(size_position, static_cast( + writer.size_bytes() - artifact_begin)); + } + + /** + * @brief Restore one packed-vector artifact and advance @p reader on success. + * + * @details Owner restoration decodes canonical words into independent + * aligned storage. View restoration retains an exact span into the reader's + * backing bytes, which must remain alive and immutable. Views require native + * little-endian word order and eight-byte-aligned payload bytes. Quick mode + * validates complete framing and safe dimensions; full mode also requires + * unused high bits in the final logical word to be zero. Failure leaves + * @p reader unchanged. + * + * @throws std::invalid_argument for malformed, incompatible, unaligned, or + * noncanonical input. + * @throws std::length_error for unrepresentable dimensions. + */ + static BasicPackedIntegerVector deserialize_impl( + BinaryReader& reader, + DeserializationValidation validation = + DeserializationValidation::kQuick) { + BinaryReader candidate = reader; + const std::size_t available_size = candidate.remaining(); + detail::require_magic(candidate, kSerializationMagic); + if (candidate.read_u32() != kSerializationVersion || + candidate.read_u8() != detail::kLittleEndianMarker || + candidate.read_u8() != kValueDigits) { + throw std::invalid_argument( + "Incompatible serialized packed integer vector"); + } + const std::size_t width = candidate.read_u8(); + if (candidate.read_u8() != 0 || width > kValueDigits) { + throw std::invalid_argument( + "Invalid serialized packed integer-vector width or reserved field"); + } + const std::size_t artifact_size = detail::checked_artifact_size( + candidate.read_u64(), kSerializationHeaderBytes, available_size); + const std::size_t count = candidate.read_size(); + const Dimensions dimensions = checked_dimensions(count, width); + if ((count == 0 && width != 0) || + dimensions.byte_count != artifact_size - kSerializationHeaderBytes) { + throw std::invalid_argument( + "Serialized packed integer vector has inconsistent dimensions"); + } + BinaryReader payload = candidate.read_subreader(dimensions.byte_count); + + if (validation == DeserializationValidation::kFull) { + BinaryReader validation_reader(payload.remaining_bytes()); + bool has_nonzero_word = false; + for (std::size_t i = 0; i < dimensions.word_count; ++i) { + const std::uint64_t word = validation_reader.read_u64(); + has_nonzero_word = has_nonzero_word || word != 0; + if (i + 1 == dimensions.word_count && dimensions.bit_count % 64 != 0 && + (word & ~low_bits_mask(dimensions.bit_count % 64)) != 0) { + throw std::invalid_argument( + "Serialized packed integer vector has non-zero unused bits"); + } + } + if (count != 0 && width != 0 && !has_nonzero_word) { + throw std::invalid_argument( + "Serialized all-zero integer vector has noncanonical width"); + } + } + + Storage storage; + if constexpr (std::same_as) { + storage = AlignedStorage(word_aligned_bit_count(dimensions.word_count)); + auto words = storage.writable_words64(); + for (std::size_t i = 0; i < dimensions.word_count; ++i) { + words[i] = payload.read_u64(); + } + } else { + if constexpr (std::endian::native != std::endian::little) { + throw std::invalid_argument( + "Packed integer-vector views require little-endian word order"); + } + const auto bytes = payload.read_bytes(dimensions.byte_count); + if (dimensions.byte_count != 0 && + reinterpret_cast(bytes.data()) % + alignof(std::uint64_t) != + 0) { + throw std::invalid_argument( + "Serialized packed integer-vector payload is not word aligned"); + } + storage = ReadOnlyStorageView(bytes); + } + if (!payload.empty()) { + throw std::invalid_argument( + "Serialized packed integer vector has trailing payload bytes"); + } + + BasicPackedIntegerVector result(std::move(storage), count, width, + LoadTag{}); + reader = candidate; + return result; + } + + private: + struct Dimensions { + std::size_t bit_count; + std::size_t word_count; + std::size_t byte_count; + }; + struct InferredTag {}; + struct ExplicitTag {}; + struct LoadTag {}; + + inline static constexpr std::array kSerializationMagic = { + 'P', 'X', 'I', 'N', 'T', 'V', 'E', 'C'}; + static constexpr std::uint32_t kSerializationVersion = 1; + static constexpr std::size_t kSerializationHeaderBytes = 32; + static constexpr std::size_t kValueDigits = + std::numeric_limits::digits; + + BasicPackedIntegerVector(std::span values, + size_type width, + InferredTag) + requires std::same_as + : BasicPackedIntegerVector(values, width, ExplicitTag{}) {} + + BasicPackedIntegerVector(std::span values, + size_type width, + ExplicitTag) + requires std::same_as + : size_(values.size()) { + if (width > kValueDigits) { + throw std::invalid_argument("Packed integer-vector width is too large"); + } + bool all_zero = true; + for (const value_type value : values) { + all_zero = all_zero && value == 0; + if (required_width(value) > width) { + throw std::invalid_argument( + "Packed integer-vector value does not fit the width"); + } + } + width_ = all_zero ? 0 : width; + const Dimensions dimensions = checked_dimensions(size_, width_); + storage_ = AlignedStorage(word_aligned_bit_count(dimensions.word_count)); + auto words = storage_.writable_words64(); + for (std::size_t i = 0; i < values.size(); ++i) { + write_field(words, i, width_, values[i]); + } + } + + BasicPackedIntegerVector(Storage storage, + size_type size, + size_type width, + LoadTag) + : storage_(std::move(storage)), size_(size), width_(width) {} + + static size_type required_width(value_type value) { + return static_cast(std::bit_width(value)); + } + + static constexpr std::uint64_t low_bits_mask(size_type count) { + return count >= 64 ? std::numeric_limits::max() + : (std::uint64_t{1} << count) - 1; + } + + static size_type inferred_width(std::span values) { + size_type width = 0; + for (const value_type value : values) { + width = std::max(width, required_width(value)); + } + return width; + } + + static Dimensions checked_dimensions(size_type count, size_type width) { + if (width > kValueDigits) { + throw std::invalid_argument("Packed integer-vector width is too large"); + } + if (width != 0 && count > std::numeric_limits::max() / width) { + throw std::length_error("Packed integer-vector bit count is too large"); + } + const size_type bit_count = count * width; + const size_type word_count = bit_count == 0 ? 0 : 1 + (bit_count - 1) / 64; + if (word_count > + std::numeric_limits::max() / sizeof(std::uint64_t)) { + throw std::length_error("Packed integer-vector payload is too large"); + } + return {bit_count, word_count, word_count * sizeof(std::uint64_t)}; + } + + static size_type word_aligned_bit_count(size_type word_count) { + constexpr size_type kWordBits = std::numeric_limits::digits; + if (word_count > std::numeric_limits::max() / kWordBits) { + throw std::length_error("Packed integer-vector storage is too large"); + } + return word_count * kWordBits; + } + + std::span logical_words() const { + const Dimensions dimensions = checked_dimensions(size_, width_); + return storage_.as_words64().first(dimensions.word_count); + } + + static value_type read_field(std::span words, + size_type position, + size_type width) { + if (width == 0) { + return 0; + } + if (width == 64) { + return static_cast(words[position]); + } + const size_type bit_position = position * width; + const size_type word = bit_position / 64; + const size_type offset = bit_position % 64; + std::uint64_t value = words[word] >> offset; + if (offset + width > 64) { + value |= words[word + 1] << (64 - offset); + } + return static_cast(value & low_bits_mask(width)); + } + + static void write_field(std::span words, + size_type position, + size_type width, + value_type value) { + if (width == 0) { + return; + } + if (width == 64) { + words[position] = static_cast(value); + return; + } + const size_type bit_position = position * width; + const size_type word = bit_position / 64; + const size_type offset = bit_position % 64; + words[word] |= static_cast(value) << offset; + if (offset + width > 64) { + words[word + 1] |= static_cast(value) >> (64 - offset); + } + } + + Storage storage_; + size_type size_ = 0; + size_type width_ = 0; +}; + +/** @brief Owning aligned packed integer vector. */ +template +using PackedIntegerVector = BasicPackedIntegerVector; + +/** + * @brief Zero-copy read-only packed integer-vector view. + * @details A deserialized view borrows its aligned immutable artifact bytes. + */ +template +using PackedIntegerVectorView = + BasicPackedIntegerVector; + +} // namespace pixie diff --git a/include/pixie/integer_vector/packed_monotone.h b/include/pixie/integer_vector/packed_monotone.h new file mode 100644 index 0000000..6214b7f --- /dev/null +++ b/include/pixie/integer_vector/packed_monotone.h @@ -0,0 +1,29 @@ +#pragma once + +/** + * @file packed_monotone.h + * @brief Monotone integer vectors backed by runtime-width packing. + */ + +#include +#include + +#include + +namespace pixie { + +/** @brief Owning validated monotone adapter over a packed integer vector. */ +template +using PackedMonotoneIntegerVector = + MonotoneIntegerVector>; + +/** + * @brief Validated monotone adapter over a zero-copy packed-vector view. + * @details A deserialized instance borrows its aligned immutable artifact + * bytes. + */ +template +using PackedMonotoneIntegerVectorView = + MonotoneIntegerVector>; + +} // namespace pixie diff --git a/src/benchmarks/integer_vector_benchmarks.cpp b/src/benchmarks/integer_vector_benchmarks.cpp new file mode 100644 index 0000000..047c5f9 --- /dev/null +++ b/src/benchmarks/integer_vector_benchmarks.cpp @@ -0,0 +1,376 @@ +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +constexpr std::size_t kQueryCount = 1 << 16; +constexpr double kWarmupSeconds = 0.1; +constexpr double kMinimumSeconds = 0.5; + +enum class Dataset : std::int64_t { + kAllZero, + kFixedWidth, + kSkewed, + kDenseMonotone, + kDuplicateMonotone, + kSparseMonotone, +}; + +constexpr std::array kDatasets = { + Dataset::kAllZero, + Dataset::kFixedWidth, + Dataset::kSkewed, + Dataset::kDenseMonotone, + Dataset::kDuplicateMonotone, + Dataset::kSparseMonotone, +}; + +constexpr std::array kMonotoneDatasets = { + Dataset::kDenseMonotone, + Dataset::kDuplicateMonotone, + Dataset::kSparseMonotone, +}; + +std::uint64_t splitmix64(std::uint64_t value) { + value += 0x9e3779b97f4a7c15ULL; + value = (value ^ (value >> 30)) * 0xbf58476d1ce4e5b9ULL; + value = (value ^ (value >> 27)) * 0x94d049bb133111ebULL; + return value ^ (value >> 31); +} + +const char* dataset_name(Dataset dataset) { + switch (dataset) { + case Dataset::kAllZero: + return "all_zero"; + case Dataset::kFixedWidth: + return "fixed_width"; + case Dataset::kSkewed: + return "skewed"; + case Dataset::kDenseMonotone: + return "dense_monotone"; + case Dataset::kDuplicateMonotone: + return "duplicate_monotone"; + case Dataset::kSparseMonotone: + return "sparse_monotone"; + } + return "unknown"; +} + +std::vector make_values(std::size_t size, Dataset dataset) { + std::vector values(size); + std::uint64_t sparse_value = 0; + for (std::size_t i = 0; i < size; ++i) { + const std::uint64_t random = splitmix64(42 + i); + switch (dataset) { + case Dataset::kAllZero: + values[i] = 0; + break; + case Dataset::kFixedWidth: + values[i] = random & ((std::uint64_t{1} << 20) - 1); + break; + case Dataset::kSkewed: + values[i] = + i % 32 == 0 ? random & ((std::uint64_t{1} << 20) - 1) : random & 15; + break; + case Dataset::kDenseMonotone: + values[i] = i; + break; + case Dataset::kDuplicateMonotone: + values[i] = i / 16; + break; + case Dataset::kSparseMonotone: + sparse_value += 1 + (random & 1023); + values[i] = sparse_value; + break; + } + } + return values; +} + +template +std::vector serialize_to_vector(const Serializable& value) { + pixie::VectorOutputSink sink; + pixie::BinaryWriter writer(sink); + value.serialize(writer); + writer.finish(); + return sink.take(); +} + +class AlignedArtifact { + public: + explicit AlignedArtifact(std::span bytes) + : words_((bytes.size() + 7) / 8), size_(bytes.size()) { + std::ranges::copy(bytes, writable_bytes().begin()); + } + + std::span bytes() const { + return std::as_bytes(std::span(words_)).first(size_); + } + + private: + std::span writable_bytes() { + return std::as_writable_bytes(std::span(words_)) + .first(size_); + } + + std::vector words_; + std::size_t size_; +}; + +Dataset selected_dataset(const benchmark::State& state) { + return static_cast(state.range(1)); +} + +std::vector make_positions(std::size_t size) { + std::vector positions(kQueryCount); + for (std::size_t i = 0; i < positions.size(); ++i) { + positions[i] = static_cast(splitmix64(117 + i) % size); + } + return positions; +} + +void set_counters(benchmark::State& state, + std::size_t size, + std::size_t width, + std::size_t artifact_bytes, + std::size_t allocated_bytes) { + state.counters["N"] = static_cast(size); + state.counters["width"] = static_cast(width); + state.counters["artifact_bytes"] = static_cast(artifact_bytes); + state.counters["allocated_bytes"] = static_cast(allocated_bytes); + state.counters["allocated_bits_per_value"] = + size == 0 ? 0.0 : static_cast(allocated_bytes) * 8.0 / size; +} + +void BM_PackedConstruction(benchmark::State& state) { + const std::size_t size = static_cast(state.range(0)); + const auto values = make_values(size, selected_dataset(state)); + const pixie::PackedIntegerVector<> sample(values); + const std::size_t artifact_bytes = serialize_to_vector(sample).size(); + for (auto _ : state) { + pixie::PackedIntegerVector<> vector(values); + benchmark::DoNotOptimize(vector); + benchmark::ClobberMemory(); + } + set_counters(state, size, sample.width(), artifact_bytes, + sample.memory_usage_bytes() - sizeof(sample)); + state.SetItemsProcessed(state.iterations() * static_cast(size)); +} + +void BM_PackedViewConstruction(benchmark::State& state) { + const std::size_t size = static_cast(state.range(0)); + const auto values = make_values(size, selected_dataset(state)); + const pixie::PackedIntegerVector<> owner(values); + const auto serialized = serialize_to_vector(owner); + const AlignedArtifact artifact(serialized); + for (auto _ : state) { + pixie::BinaryReader reader(artifact.bytes()); + auto view = pixie::PackedIntegerVectorView<>::deserialize(reader); + benchmark::DoNotOptimize(view); + } + set_counters(state, size, owner.width(), artifact.bytes().size(), 0); + state.SetItemsProcessed(state.iterations() * static_cast(size)); +} + +template +void BM_PackedAccess(benchmark::State& state) { + const std::size_t size = static_cast(state.range(0)); + const auto values = make_values(size, selected_dataset(state)); + const pixie::PackedIntegerVector<> owner(values); + const auto serialized = serialize_to_vector(owner); + const AlignedArtifact artifact(serialized); + pixie::BinaryReader reader(artifact.bytes()); + const auto view = pixie::PackedIntegerVectorView<>::deserialize(reader); + const auto positions = make_positions(size); + std::size_t query = 0; + for (auto _ : state) { + const std::size_t position = positions[query++ & (kQueryCount - 1)]; + if constexpr (View) { + benchmark::DoNotOptimize(view[position]); + } else { + benchmark::DoNotOptimize(owner[position]); + } + } + set_counters(state, size, owner.width(), artifact.bytes().size(), + View ? 0 : owner.memory_usage_bytes() - sizeof(owner)); + state.SetItemsProcessed(state.iterations()); +} + +void BM_RawSpanAccess(benchmark::State& state) { + const std::size_t size = static_cast(state.range(0)); + const auto values = make_values(size, selected_dataset(state)); + const std::span span(values); + const auto positions = make_positions(size); + std::size_t query = 0; + for (auto _ : state) { + std::uint64_t value = span[positions[query++ & (kQueryCount - 1)]]; + benchmark::DoNotOptimize(value); + } + const std::size_t width = + values.empty() ? 0 : std::bit_width(*std::ranges::max_element(values)); + set_counters(state, size, width, values.size() * sizeof(std::uint64_t), + values.capacity() * sizeof(std::uint64_t)); + state.SetItemsProcessed(state.iterations()); +} + +template +void BM_PackedCopy(benchmark::State& state) { + const std::size_t size = static_cast(state.range(0)); + const auto values = make_values(size, selected_dataset(state)); + const pixie::PackedIntegerVector<> owner(values); + const auto serialized = serialize_to_vector(owner); + const AlignedArtifact artifact(serialized); + pixie::BinaryReader reader(artifact.bytes()); + const auto view = pixie::PackedIntegerVectorView<>::deserialize(reader); + constexpr std::size_t kCopyCount = 256; + std::vector output(kCopyCount); + const auto positions = make_positions(size - kCopyCount + 1); + std::size_t query = 0; + for (auto _ : state) { + const std::size_t begin = positions[query++ & (kQueryCount - 1)]; + if constexpr (View) { + view.copy_to(begin, output); + } else { + owner.copy_to(begin, output); + } + benchmark::ClobberMemory(); + } + set_counters(state, size, owner.width(), artifact.bytes().size(), + View ? 0 : owner.memory_usage_bytes() - sizeof(owner)); + state.SetItemsProcessed(state.iterations() * kCopyCount); +} + +template +void BM_MonotoneBound(benchmark::State& state) { + const std::size_t size = static_cast(state.range(0)); + const auto values = make_values(size, selected_dataset(state)); + const pixie::PackedMonotoneIntegerVector<> owner{ + pixie::PackedIntegerVector<>(values)}; + const auto serialized = serialize_to_vector(owner); + const AlignedArtifact artifact(serialized); + pixie::BinaryReader reader(artifact.bytes()); + const auto view = + pixie::PackedMonotoneIntegerVectorView<>::deserialize(reader); + std::vector queries(kQueryCount); + for (std::size_t i = 0; i < queries.size(); ++i) { + const std::uint64_t value = values[splitmix64(i + 991) % size]; + queries[i] = + i % 2 == 0 || value == std::numeric_limits::max() + ? value + : value + 1; + } + std::size_t query = 0; + for (auto _ : state) { + const std::uint64_t value = queries[query++ & (kQueryCount - 1)]; + if constexpr (View && Upper) { + benchmark::DoNotOptimize(view.upper_bound_index(value)); + } else if constexpr (View) { + benchmark::DoNotOptimize(view.lower_bound_index(value)); + } else if constexpr (Upper) { + benchmark::DoNotOptimize(owner.upper_bound_index(value)); + } else { + benchmark::DoNotOptimize(owner.lower_bound_index(value)); + } + } + const std::size_t width = values.empty() ? 0 : std::bit_width(values.back()); + set_counters(state, size, width, artifact.bytes().size(), + View ? 0 : owner.memory_usage_bytes() - sizeof(owner)); + state.SetItemsProcessed(state.iterations()); +} + +void configure(benchmark::internal::Benchmark* row, + std::size_t size, + Dataset dataset, + benchmark::TimeUnit unit) { + row->Args( + {static_cast(size), static_cast(dataset)}) + ->ArgNames({"N", "dataset"}) + ->Unit(unit) + ->MinWarmUpTime(kWarmupSeconds) + ->MinTime(kMinimumSeconds); +} + +void register_benchmarks() { + constexpr std::array general_sizes = {1 << 10, 1 << 16, + 1 << 20}; + for (const std::size_t size : general_sizes) { + for (const Dataset dataset : kDatasets) { + const std::string suffix = std::string("/") + dataset_name(dataset); + configure(benchmark::RegisterBenchmark( + ("integer_vector/construct_owner" + suffix).c_str(), + &BM_PackedConstruction), + size, dataset, benchmark::kMillisecond); + configure(benchmark::RegisterBenchmark( + ("integer_vector/construct_view" + suffix).c_str(), + &BM_PackedViewConstruction), + size, dataset, benchmark::kNanosecond); + configure(benchmark::RegisterBenchmark( + ("integer_vector/access_owner" + suffix).c_str(), + &BM_PackedAccess), + size, dataset, benchmark::kNanosecond); + configure(benchmark::RegisterBenchmark( + ("integer_vector/access_view" + suffix).c_str(), + &BM_PackedAccess), + size, dataset, benchmark::kNanosecond); + configure(benchmark::RegisterBenchmark( + ("integer_vector/access_raw_span" + suffix).c_str(), + &BM_RawSpanAccess), + size, dataset, benchmark::kNanosecond); + configure(benchmark::RegisterBenchmark( + ("integer_vector/copy_owner" + suffix).c_str(), + &BM_PackedCopy), + size, dataset, benchmark::kNanosecond); + configure(benchmark::RegisterBenchmark( + ("integer_vector/copy_view" + suffix).c_str(), + &BM_PackedCopy), + size, dataset, benchmark::kNanosecond); + } + } + + constexpr std::array bound_sizes = { + std::size_t{1} << 10, std::size_t{1} << 14, std::size_t{1} << 18, + std::size_t{1} << 22, std::size_t{1} << 26}; + for (const std::size_t size : bound_sizes) { + for (const Dataset dataset : kMonotoneDatasets) { + const std::string suffix = std::string("/") + dataset_name(dataset); + configure(benchmark::RegisterBenchmark( + ("integer_vector/lower_bound_owner" + suffix).c_str(), + &BM_MonotoneBound), + size, dataset, benchmark::kNanosecond); + configure(benchmark::RegisterBenchmark( + ("integer_vector/lower_bound_view" + suffix).c_str(), + &BM_MonotoneBound), + size, dataset, benchmark::kNanosecond); + configure(benchmark::RegisterBenchmark( + ("integer_vector/upper_bound_owner" + suffix).c_str(), + &BM_MonotoneBound), + size, dataset, benchmark::kNanosecond); + configure(benchmark::RegisterBenchmark( + ("integer_vector/upper_bound_view" + suffix).c_str(), + &BM_MonotoneBound), + size, dataset, benchmark::kNanosecond); + } + } +} + +} // 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/serialization_benchmarks.cpp b/src/benchmarks/serialization_benchmarks.cpp index d1f7841..046e9da 100644 --- a/src/benchmarks/serialization_benchmarks.cpp +++ b/src/benchmarks/serialization_benchmarks.cpp @@ -1,4 +1,5 @@ #include +#include #include #include #include @@ -496,6 +497,134 @@ void BM_WaveletTreeSerialize(benchmark::State& state) { set_artifact_counters(state, symbol_count, artifact.size()); } +std::vector make_integer_vector_values(std::size_t size, + bool monotone) { + std::vector values(size); + for (std::size_t i = 0; i < size; ++i) { + values[i] = (i * 0x9e3779b97f4a7c15ULL) >> 56; + } + if (monotone) { + std::ranges::sort(values); + } + return values; +} + +template +Owner make_integer_vector_owner(std::size_t size); + +template <> +pixie::PackedIntegerVector<> make_integer_vector_owner(std::size_t size) { + const auto values = make_integer_vector_values(size, false); + return pixie::PackedIntegerVector<>(values); +} + +template <> +pixie::PackedMonotoneIntegerVector<> make_integer_vector_owner( + std::size_t size) { + const auto values = make_integer_vector_values(size, true); + return pixie::PackedMonotoneIntegerVector<>{ + pixie::PackedIntegerVector<>(values)}; +} + +template +void integer_vector_serialize_owner(benchmark::State& state) { + const std::size_t size = static_cast(state.range(0)); + const Owner owner = make_integer_vector_owner(size); + const std::vector artifact = serialize_to_vector(owner); + std::vector destination(artifact.size()); + std::vector staging(kDefaultStagingBytes); + serialize_iterations(state, owner, as_writable_span(destination), + as_writable_span(staging)); + set_artifact_counters(state, size, artifact.size()); +} + +template +void integer_vector_serialize_view(benchmark::State& state) { + const std::size_t size = static_cast(state.range(0)); + const Owner owner = make_integer_vector_owner(size); + const std::vector serialized = serialize_to_vector(owner); + const AlignedArtifact artifact(as_const_span(serialized)); + pixie::BinaryReader reader(artifact.bytes()); + const View view = View::deserialize(reader); + std::vector destination(serialized.size()); + std::vector staging(kDefaultStagingBytes); + serialize_iterations(state, view, as_writable_span(destination), + as_writable_span(staging)); + set_artifact_counters(state, size, serialized.size()); +} + +void BM_PackedIntegerVectorSerializeOwning(benchmark::State& state) { + integer_vector_serialize_owner>(state); +} + +void BM_PackedIntegerVectorSerializeView(benchmark::State& state) { + integer_vector_serialize_view, + pixie::PackedIntegerVectorView<>>(state); +} + +void BM_PackedMonotoneIntegerVectorSerializeOwning(benchmark::State& state) { + integer_vector_serialize_owner>(state); +} + +void BM_PackedMonotoneIntegerVectorSerializeView(benchmark::State& state) { + integer_vector_serialize_view, + pixie::PackedMonotoneIntegerVectorView<>>( + state); +} + +template +void integer_vector_deserialize_owner(benchmark::State& state) { + const std::size_t size = static_cast(state.range(0)); + const Owner owner = make_integer_vector_owner(size); + const std::vector artifact = serialize_to_vector(owner); + deserialize_iterations(state, as_const_span(artifact), + [](pixie::BinaryReader& reader) { + return Owner::deserialize(reader, Validation); + }); + set_artifact_counters(state, size, artifact.size()); +} + +template +void integer_vector_deserialize_view(benchmark::State& state) { + const std::size_t size = static_cast(state.range(0)); + const Owner owner = make_integer_vector_owner(size); + const std::vector serialized = serialize_to_vector(owner); + const AlignedArtifact artifact(as_const_span(serialized)); + deserialize_iterations(state, artifact.bytes(), + [](pixie::BinaryReader& reader) { + return View::deserialize(reader, Validation); + }); + set_artifact_counters(state, size, artifact.bytes().size()); +} + +template +void BM_PackedIntegerVectorDeserializeOwningImpl(benchmark::State& state) { + integer_vector_deserialize_owner, Validation>( + state); +} + +template +void BM_PackedIntegerVectorDeserializeViewImpl(benchmark::State& state) { + integer_vector_deserialize_view, + pixie::PackedIntegerVectorView<>, Validation>( + state); +} + +template +void BM_PackedMonotoneIntegerVectorDeserializeOwningImpl( + benchmark::State& state) { + integer_vector_deserialize_owner, + Validation>(state); +} + +template +void BM_PackedMonotoneIntegerVectorDeserializeViewImpl( + benchmark::State& state) { + integer_vector_deserialize_view, + pixie::PackedMonotoneIntegerVectorView<>, + Validation>(state); +} + template void BM_WaveletTreeDeserializeViewImpl(benchmark::State& state) { const std::size_t symbol_count = static_cast(state.range(0)); @@ -524,6 +653,10 @@ PIXIE_DESERIALIZATION_WRAPPERS(BM_RankSelectDeserializeView) PIXIE_DESERIALIZATION_WRAPPERS(BM_RmMDeserialize) PIXIE_DESERIALIZATION_WRAPPERS(BM_RmqDeserialize) PIXIE_DESERIALIZATION_WRAPPERS(BM_WaveletTreeDeserializeView) +PIXIE_DESERIALIZATION_WRAPPERS(BM_PackedIntegerVectorDeserializeOwning) +PIXIE_DESERIALIZATION_WRAPPERS(BM_PackedIntegerVectorDeserializeView) +PIXIE_DESERIALIZATION_WRAPPERS(BM_PackedMonotoneIntegerVectorDeserializeOwning) +PIXIE_DESERIALIZATION_WRAPPERS(BM_PackedMonotoneIntegerVectorDeserializeView) #undef PIXIE_DESERIALIZATION_WRAPPERS @@ -613,6 +746,18 @@ PIXIE_STRUCTURE_BENCHMARK(BM_RmqDeserializeFull); PIXIE_STRUCTURE_BENCHMARK(BM_WaveletTreeSerialize); PIXIE_STRUCTURE_BENCHMARK(BM_WaveletTreeDeserializeViewQuick); PIXIE_STRUCTURE_BENCHMARK(BM_WaveletTreeDeserializeViewFull); +PIXIE_STRUCTURE_BENCHMARK(BM_PackedIntegerVectorSerializeOwning); +PIXIE_STRUCTURE_BENCHMARK(BM_PackedIntegerVectorSerializeView); +PIXIE_STRUCTURE_BENCHMARK(BM_PackedIntegerVectorDeserializeOwningQuick); +PIXIE_STRUCTURE_BENCHMARK(BM_PackedIntegerVectorDeserializeOwningFull); +PIXIE_STRUCTURE_BENCHMARK(BM_PackedIntegerVectorDeserializeViewQuick); +PIXIE_STRUCTURE_BENCHMARK(BM_PackedIntegerVectorDeserializeViewFull); +PIXIE_STRUCTURE_BENCHMARK(BM_PackedMonotoneIntegerVectorSerializeOwning); +PIXIE_STRUCTURE_BENCHMARK(BM_PackedMonotoneIntegerVectorSerializeView); +PIXIE_STRUCTURE_BENCHMARK(BM_PackedMonotoneIntegerVectorDeserializeOwningQuick); +PIXIE_STRUCTURE_BENCHMARK(BM_PackedMonotoneIntegerVectorDeserializeOwningFull); +PIXIE_STRUCTURE_BENCHMARK(BM_PackedMonotoneIntegerVectorDeserializeViewQuick); +PIXIE_STRUCTURE_BENCHMARK(BM_PackedMonotoneIntegerVectorDeserializeViewFull); #undef PIXIE_STRUCTURE_BENCHMARK #undef PIXIE_SERIALIZATION_TIMING diff --git a/src/tests/benchmark_tests.cpp b/src/tests/benchmark_tests.cpp index dbc3bc2..fd65d14 100644 --- a/src/tests/benchmark_tests.cpp +++ b/src/tests/benchmark_tests.cpp @@ -1,6 +1,6 @@ #include #include -#include +#include #include #include diff --git a/src/tests/file_archive_tests.cpp b/src/tests/file_archive_tests.cpp index 62bfe6f..da397b3 100644 --- a/src/tests/file_archive_tests.cpp +++ b/src/tests/file_archive_tests.cpp @@ -1,5 +1,5 @@ #include -#include +#include #include #include diff --git a/src/tests/integer_vector_tests.cpp b/src/tests/integer_vector_tests.cpp new file mode 100644 index 0000000..5f0d944 --- /dev/null +++ b/src/tests/integer_vector_tests.cpp @@ -0,0 +1,656 @@ +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +static_assert(pixie::IntegerVectorValue); +static_assert(pixie::IntegerVectorValue); +static_assert(!pixie::IntegerVectorValue); +static_assert(!pixie::IntegerVectorValue); +static_assert(!pixie::IntegerVectorValue); +static_assert(!pixie::IntegerVectorValue); + +struct MinimalIntegerVector + : pixie::IntegerVectorBase { + std::size_t size_impl() const { return 0; } + std::uint64_t value_at_impl(std::size_t) const { return 0; } + void copy_to_impl(std::size_t, std::span) const {} +}; + +template +concept HasMemoryUsage = + requires(const Vector& vector) { vector.memory_usage_bytes(); }; + +static_assert( + !HasMemoryUsage>); + +using PackedOwner = pixie::PackedIntegerVector<>; +using PackedView = pixie::PackedIntegerVectorView<>; +using PackedMonotoneOwner = pixie::PackedMonotoneIntegerVector<>; +using PackedMonotoneView = pixie::PackedMonotoneIntegerVectorView<>; +using MinimalMonotone = pixie::MonotoneIntegerVector; + +static_assert(pixie::Serializable); +static_assert(pixie::Serializable); +static_assert(pixie::Serializable); +static_assert(pixie::Serializable); +static_assert(pixie::Deserializable); +static_assert(pixie::Deserializable); +static_assert(pixie::Deserializable); +static_assert(pixie::Deserializable); +static_assert(!pixie::Serializable); +static_assert(!pixie::Deserializable); +static_assert(std::same_as())), + PackedOwner>); +static_assert(std::same_as())), + PackedMonotoneView>); + +template +std::vector serialize_to_bytes(const Serializable& value) { + pixie::VectorOutputSink sink; + pixie::BinaryWriter writer(sink); + value.serialize(writer); + writer.finish(); + return sink.take(); +} + +class AlignedArtifact { + public: + explicit AlignedArtifact(std::span bytes) + : words_((bytes.size() + sizeof(std::uint64_t) - 1) / + sizeof(std::uint64_t)), + size_(bytes.size()) { + std::ranges::copy(bytes, writable_bytes().begin()); + } + + std::span bytes() const { + return std::as_bytes(std::span(words_)).first(size_); + } + + std::span writable_bytes() { + return std::as_writable_bytes(std::span(words_)) + .first(size_); + } + + private: + std::vector words_; + std::size_t size_; +}; + +template +void overwrite_little_endian(std::span bytes, + std::size_t offset, + Integer value) { + using Unsigned = std::make_unsigned_t; + const Unsigned encoded = static_cast(value); + ASSERT_LE(offset + sizeof(Integer), bytes.size()); + for (std::size_t i = 0; i < sizeof(Integer); ++i) { + bytes[offset + i] = + static_cast((encoded >> (i * 8)) & Unsigned{0xff}); + } +} + +template +struct VectorHolder { + std::shared_ptr backing; + Vector vector; +}; + +struct PackedOwnerCase { + using Vector = pixie::PackedIntegerVector<>; + static constexpr bool kOwnsPayload = true; + + static VectorHolder make(std::span values) { + return {nullptr, Vector(values)}; + } + + static VectorHolder make(std::span values, + std::size_t width) { + return {nullptr, Vector(values, width)}; + } +}; + +struct PackedViewCase { + using Vector = pixie::PackedIntegerVectorView<>; + static constexpr bool kOwnsPayload = false; + + static VectorHolder make(std::span values) { + return make_from_owner(pixie::PackedIntegerVector<>(values)); + } + + static VectorHolder make(std::span values, + std::size_t width) { + return make_from_owner(pixie::PackedIntegerVector<>(values, width)); + } + + private: + static VectorHolder make_from_owner( + const pixie::PackedIntegerVector<>& owner) { + const std::vector serialized = serialize_to_bytes(owner); + auto backing = std::make_shared( + std::span(serialized)); + pixie::BinaryReader reader(backing->bytes()); + Vector vector = + Vector::deserialize(reader, pixie::DeserializationValidation::kFull); + EXPECT_TRUE(reader.empty()); + return {std::move(backing), std::move(vector)}; + } +}; + +template +class PackedIntegerVectorSpecificationTest : public ::testing::Test {}; + +using PackedIntegerVectorCases = + ::testing::Types; +TYPED_TEST_SUITE(PackedIntegerVectorSpecificationTest, + PackedIntegerVectorCases); + +TYPED_TEST(PackedIntegerVectorSpecificationTest, EmptyAndAllZeroUseWidthZero) { + const typename TypeParam::Vector default_vector; + EXPECT_TRUE(default_vector.empty()); + EXPECT_EQ(default_vector.width(), 0u); + + const std::vector empty; + const auto empty_result = TypeParam::make(empty); + EXPECT_TRUE(empty_result.vector.empty()); + EXPECT_EQ(empty_result.vector.width(), 0u); + + const std::array zeros{}; + const auto zero_result = TypeParam::make(zeros, 37); + EXPECT_EQ(zero_result.vector.size(), zeros.size()); + EXPECT_EQ(zero_result.vector.width(), 0u); + for (std::size_t i = 0; i < zeros.size(); ++i) { + EXPECT_EQ(zero_result.vector[i], 0u); + } +} + +TYPED_TEST(PackedIntegerVectorSpecificationTest, SupportsEveryRuntimeWidth) { + for (std::size_t width = 1; width <= 64; ++width) { + const std::uint64_t maximum = + width == 64 ? std::numeric_limits::max() + : (std::uint64_t{1} << width) - 1; + const std::array values = {std::uint64_t{0}, maximum, maximum / 3, + std::uint64_t{1}}; + const auto inferred = TypeParam::make(values); + ASSERT_EQ(inferred.vector.width(), width); + const auto result = TypeParam::make(values, width); + ASSERT_EQ(result.vector.width(), width); + ASSERT_EQ(result.vector.size(), values.size()); + for (std::size_t i = 0; i < values.size(); ++i) { + EXPECT_EQ(result.vector[i], values[i]); + EXPECT_EQ(result.vector.at(i), values[i]); + } + } +} + +TYPED_TEST(PackedIntegerVectorSpecificationTest, + InfersWidthsAndCrossesWordBoundaries) { + for (const std::size_t width : {1u, 3u, 7u, 17u, 31u, 63u, 64u}) { + const std::uint64_t mask = width == 64 + ? std::numeric_limits::max() + : (std::uint64_t{1} << width) - 1; + std::vector values(517); + for (std::size_t i = 0; i < values.size(); ++i) { + values[i] = (i * 0x9e3779b97f4a7c15ULL) & mask; + } + values.back() = mask; + const auto result = TypeParam::make(values); + EXPECT_EQ(result.vector.width(), width); + for (std::size_t i = 0; i < values.size(); ++i) { + ASSERT_EQ(result.vector[i], values[i]) << "width=" << width << " i=" << i; + } + } +} + +TYPED_TEST(PackedIntegerVectorSpecificationTest, + CopyToChecksCompleteRangeBeforeWriting) { + const std::array values = {std::uint64_t{3}, std::uint64_t{1}, + std::uint64_t{4}, std::uint64_t{1}, + std::uint64_t{5}, std::uint64_t{9}}; + const auto result = TypeParam::make(values); + + std::array middle{}; + result.vector.copy_to(2, middle); + EXPECT_EQ(middle, (std::array{4, 1, 5})); + std::span empty; + result.vector.copy_to(result.vector.size(), empty); + + std::array unchanged = {77, 88}; + EXPECT_THROW(result.vector.copy_to(result.vector.size(), unchanged), + std::out_of_range); + EXPECT_EQ(unchanged, (std::array{77, 88})); + EXPECT_THROW(result.vector.copy_to(result.vector.size() + 1, empty), + std::out_of_range); + EXPECT_THROW((void)result.vector.at(result.vector.size()), std::out_of_range); +} + +TYPED_TEST(PackedIntegerVectorSpecificationTest, MatchesDeterministicOracle) { + std::mt19937_64 random(42); + for (std::size_t count : {1u, 2u, 63u, 64u, 65u, 511u, 512u, 513u}) { + std::vector values(count); + std::generate(values.begin(), values.end(), [&] { return random(); }); + const auto result = TypeParam::make(values); + std::vector copy(count); + result.vector.copy_to(0, copy); + EXPECT_EQ(copy, values); + } +} + +TYPED_TEST(PackedIntegerVectorSpecificationTest, ReportsOnlyOwnedMemory) { + const std::array values = {std::uint64_t{1}, std::uint64_t{2}, + std::uint64_t{3}}; + const auto result = TypeParam::make(values); + if constexpr (TypeParam::kOwnsPayload) { + EXPECT_GE(result.vector.memory_usage_bytes(), + sizeof(typename TypeParam::Vector) + 64u); + } else { + EXPECT_EQ(result.vector.memory_usage_bytes(), + sizeof(typename TypeParam::Vector)); + } +} + +TEST(PackedIntegerVectorTest, RejectsInvalidExplicitWidths) { + const std::array values = {std::uint64_t{0}, std::uint64_t{4}}; + EXPECT_THROW((void)pixie::PackedIntegerVector<>(values, 2), + std::invalid_argument); + EXPECT_THROW((void)pixie::PackedIntegerVector<>(values, 65), + std::invalid_argument); + const std::array zeros = {std::uint64_t{0}, std::uint64_t{0}}; + EXPECT_NO_THROW((void)pixie::PackedIntegerVector<>(zeros, 0)); +} + +template +void check_monotone_queries(const Monotone& vector, + const std::vector& values) { + const std::array probes = {std::uint64_t{0}, + std::uint64_t{1}, + std::uint64_t{2}, + std::uint64_t{3}, + std::uint64_t{7}, + std::uint64_t{8}, + std::numeric_limits::max() - 1, + std::numeric_limits::max()}; + for (const std::uint64_t probe : probes) { + const std::size_t lower = static_cast( + std::lower_bound(values.begin(), values.end(), probe) - values.begin()); + const std::size_t upper = static_cast( + std::upper_bound(values.begin(), values.end(), probe) - values.begin()); + EXPECT_EQ(vector.lower_bound_index(probe), lower); + EXPECT_EQ(vector.upper_bound_index(probe), upper); + EXPECT_EQ(vector.contains(probe), lower != upper); + EXPECT_EQ(vector.count(probe), upper - lower); + } +} + +TEST(MonotoneIntegerVectorTest, PreservesDuplicatesAndMatchesStandardBounds) { + const std::vector> cases = { + {}, + {7}, + {0, 1, 2, 3, 4, 5}, + {7, 7, 7, 7}, + {0, 1, 1, 1, 2, 7, 7, 8}, + {0, 1000, 1000000, std::numeric_limits::max()}, + }; + for (const auto& values : cases) { + const pixie::PackedMonotoneIntegerVector<> vector{ + pixie::PackedIntegerVector<>(values)}; + EXPECT_EQ(vector.size(), values.size()); + check_monotone_queries(vector, values); + } +} + +TEST(MonotoneIntegerVectorTest, RejectsEveryDecreasingShape) { + for (const std::vector& values : + {std::vector{1, 0}, {0, 2, 1}, {3, 3, 2}, {9, 1, 8}}) { + EXPECT_THROW((void)pixie::PackedMonotoneIntegerVector<>( + pixie::PackedIntegerVector<>(values)), + std::invalid_argument); + } +} + +TEST(IntegerVectorSerializationTest, PackedOwnerAndViewRoundTrip) { + const std::vector values = { + 0, 1, 2, 3, 127, 128, std::numeric_limits::max()}; + const pixie::PackedIntegerVector<> original(values); + std::vector bytes = serialize_to_bytes(original); + AlignedArtifact aligned(bytes); + + for (const auto validation : {pixie::DeserializationValidation::kQuick, + pixie::DeserializationValidation::kFull}) { + pixie::BinaryReader owner_reader(bytes); + auto owner = + pixie::PackedIntegerVector<>::deserialize(owner_reader, validation); + EXPECT_TRUE(owner_reader.empty()); + pixie::BinaryReader view_reader(aligned.bytes()); + auto view = + pixie::PackedIntegerVectorView<>::deserialize(view_reader, validation); + EXPECT_TRUE(view_reader.empty()); + for (std::size_t i = 0; i < values.size(); ++i) { + EXPECT_EQ(owner[i], values[i]); + EXPECT_EQ(view[i], values[i]); + } + EXPECT_EQ(serialize_to_bytes(owner), bytes); + EXPECT_EQ(serialize_to_bytes(view), bytes); + } + + pixie::BinaryReader owner_reader(bytes); + auto independent = pixie::PackedIntegerVector<>::deserialize(owner_reader); + bytes.clear(); + bytes.shrink_to_fit(); + EXPECT_EQ(independent[values.size() - 1], values.back()); +} + +TEST(IntegerVectorSerializationTest, SupportsConcatenatedArtifactsAndSpanApi) { + const std::array first_values = {std::uint64_t{1}, std::uint64_t{2}}; + const std::array second_values = {std::uint64_t{7}, std::uint64_t{9}, + std::uint64_t{11}}; + const auto first = + serialize_to_bytes(pixie::PackedIntegerVector<>(first_values)); + const auto second = + serialize_to_bytes(pixie::PackedIntegerVector<>(second_values)); + std::vector joined = first; + joined.insert(joined.end(), second.begin(), second.end()); + + std::span remaining(joined); + const auto restored_first = + pixie::PackedIntegerVector<>::deserialize(remaining); + EXPECT_EQ(remaining.size(), second.size()); + const auto restored_second = + pixie::PackedIntegerVector<>::deserialize(remaining); + EXPECT_TRUE(remaining.empty()); + EXPECT_EQ(restored_first[1], 2u); + EXPECT_EQ(restored_second[2], 11u); +} + +TEST(IntegerVectorSerializationTest, SpanApiRollsBackOnFailure) { + const std::array values = {std::uint64_t{1}, std::uint64_t{17}, + std::uint64_t{1023}}; + std::vector truncated = + serialize_to_bytes(pixie::PackedIntegerVector<>(values)); + truncated.pop_back(); + std::span remaining(truncated); + + EXPECT_THROW((void)pixie::PackedIntegerVector<>::deserialize( + remaining, pixie::DeserializationValidation::kFull), + std::exception); + EXPECT_EQ(remaining.size(), truncated.size()); +} + +TEST(IntegerVectorSerializationTest, RejectsEveryTruncatedPackedPrefix) { + const std::array values = {std::uint64_t{1}, std::uint64_t{17}, + std::uint64_t{1023}}; + const auto bytes = serialize_to_bytes(pixie::PackedIntegerVector<>(values)); + for (std::size_t length = 0; length < bytes.size(); ++length) { + pixie::BinaryReader reader(std::span(bytes).first(length)); + EXPECT_THROW((void)pixie::PackedIntegerVector<>::deserialize(reader), + std::exception) + << "length=" << length; + EXPECT_EQ(reader.position(), 0u) << "length=" << length; + } +} + +TEST(IntegerVectorSerializationTest, RejectsEveryTruncatedMonotonePrefix) { + const std::array values = {std::uint64_t{1}, std::uint64_t{1}, + std::uint64_t{1023}}; + const pixie::PackedMonotoneIntegerVector<> original{ + pixie::PackedIntegerVector<>(values)}; + const auto bytes = serialize_to_bytes(original); + for (std::size_t length = 0; length < bytes.size(); ++length) { + pixie::BinaryReader reader(std::span(bytes).first(length)); + EXPECT_THROW( + (void)pixie::PackedMonotoneIntegerVector<>::deserialize(reader), + std::exception) + << "length=" << length; + EXPECT_EQ(reader.position(), 0u) << "length=" << length; + } +} + +TEST(IntegerVectorSerializationTest, + RejectsPackedHeaderAndDimensionCorruptionTransactionally) { + const std::array values = {std::uint64_t{1}, std::uint64_t{2}, + std::uint64_t{3}}; + const auto original = + serialize_to_bytes(pixie::PackedIntegerVector<>(values)); + const auto expect_rejected = [&](std::vector bytes) { + pixie::BinaryReader reader(bytes); + EXPECT_THROW((void)pixie::PackedIntegerVector<>::deserialize(reader), + std::exception); + EXPECT_EQ(reader.position(), 0u); + }; + + const std::array)>, 8> mutations = { + [](std::span bytes) { bytes[0] ^= std::byte{1}; }, + [](std::span bytes) { + overwrite_little_endian(bytes, 8, std::uint32_t{2}); + }, + [](std::span bytes) { bytes[12] = std::byte{2}; }, + [](std::span bytes) { bytes[13] = std::byte{32}; }, + [](std::span bytes) { bytes[14] = std::byte{65}; }, + [](std::span bytes) { bytes[15] = std::byte{1}; }, + [](std::span bytes) { + overwrite_little_endian(bytes, 16, std::uint64_t{24}); + }, + [](std::span bytes) { + overwrite_little_endian(bytes, 24, + std::numeric_limits::max()); + }, + }; + for (const auto& mutation : mutations) { + auto corrupted = original; + mutation(corrupted); + expect_rejected(std::move(corrupted)); + } + + auto trailing = original; + trailing.resize(trailing.size() + sizeof(std::uint64_t)); + overwrite_little_endian(std::span(trailing), 16, + static_cast(trailing.size())); + expect_rejected(std::move(trailing)); +} + +TEST(IntegerVectorSerializationTest, FullValidationRejectsUnusedFinalBits) { + const std::array values = {std::uint64_t{1}, std::uint64_t{2}, + std::uint64_t{3}}; + auto bytes = serialize_to_bytes(pixie::PackedIntegerVector<>(values, 2)); + bytes.back() |= std::byte{0x80}; + + pixie::BinaryReader quick_reader(bytes); + EXPECT_NO_THROW((void)pixie::PackedIntegerVector<>::deserialize( + quick_reader, pixie::DeserializationValidation::kQuick)); + pixie::BinaryReader full_reader(bytes); + EXPECT_THROW((void)pixie::PackedIntegerVector<>::deserialize( + full_reader, pixie::DeserializationValidation::kFull), + std::invalid_argument); + EXPECT_EQ(full_reader.position(), 0u); +} + +TEST(IntegerVectorSerializationTest, FullValidationRejectsNoncanonicalZeros) { + const std::array values = {std::uint64_t{1}, std::uint64_t{2}}; + auto bytes = serialize_to_bytes(pixie::PackedIntegerVector<>(values, 2)); + overwrite_little_endian(std::span(bytes), 32, std::uint64_t{0}); + + pixie::BinaryReader quick_reader(bytes); + const auto quick = pixie::PackedIntegerVector<>::deserialize( + quick_reader, pixie::DeserializationValidation::kQuick); + EXPECT_EQ(quick.width(), 2u); + EXPECT_EQ(quick[0], 0u); + pixie::BinaryReader full_reader(bytes); + EXPECT_THROW((void)pixie::PackedIntegerVector<>::deserialize( + full_reader, pixie::DeserializationValidation::kFull), + std::invalid_argument); + EXPECT_EQ(full_reader.position(), 0u); +} + +TEST(IntegerVectorSerializationTest, PackedViewRequiresAlignedPayload) { + const std::array values = {std::uint64_t{1}, std::uint64_t{2}}; + const auto bytes = serialize_to_bytes(pixie::PackedIntegerVector<>(values)); + std::vector unaligned(bytes.size() + 1); + std::ranges::copy(bytes, unaligned.begin() + 1); + const std::span artifact(unaligned.data() + 1, bytes.size()); + + pixie::BinaryReader view_reader(artifact); + EXPECT_THROW((void)pixie::PackedIntegerVectorView<>::deserialize(view_reader), + std::invalid_argument); + EXPECT_EQ(view_reader.position(), 0u); + pixie::BinaryReader owner_reader(artifact); + EXPECT_NO_THROW( + (void)pixie::PackedIntegerVector<>::deserialize(owner_reader)); +} + +TEST(IntegerVectorSerializationTest, MonotoneWrapperRoundTripsOwnerAndView) { + const std::vector values = {0, 1, 1, 7, 7, 7, 1000}; + const pixie::PackedMonotoneIntegerVector<> original{ + pixie::PackedIntegerVector<>(values)}; + const auto bytes = serialize_to_bytes(original); + AlignedArtifact aligned(bytes); + + pixie::BinaryReader owner_reader(bytes); + const auto owner = pixie::PackedMonotoneIntegerVector<>::deserialize( + owner_reader, pixie::DeserializationValidation::kFull); + pixie::BinaryReader view_reader(aligned.bytes()); + const auto view = pixie::PackedMonotoneIntegerVectorView<>::deserialize( + view_reader, pixie::DeserializationValidation::kFull); + EXPECT_TRUE(owner_reader.empty()); + EXPECT_TRUE(view_reader.empty()); + check_monotone_queries(owner, values); + check_monotone_queries(view, values); + EXPECT_EQ(serialize_to_bytes(owner), bytes); + EXPECT_EQ(serialize_to_bytes(view), bytes); +} + +TEST(IntegerVectorSerializationTest, + QuickMonotoneValidationTrustsOrderingAndFullRejectsIt) { + const std::array values = {std::uint64_t{1}, std::uint64_t{2}}; + const pixie::PackedMonotoneIntegerVector<> original{ + pixie::PackedIntegerVector<>(values, 2)}; + auto bytes = serialize_to_bytes(original); + constexpr std::size_t kWrapperHeaderBytes = 24; + constexpr std::size_t kPackedHeaderBytes = 32; + overwrite_little_endian(std::span(bytes), + kWrapperHeaderBytes + kPackedHeaderBytes, + std::uint64_t{6}); + + pixie::BinaryReader quick_reader(bytes); + EXPECT_NO_THROW((void)pixie::PackedMonotoneIntegerVector<>::deserialize( + quick_reader, pixie::DeserializationValidation::kQuick)); + pixie::BinaryReader full_reader(bytes); + EXPECT_THROW((void)pixie::PackedMonotoneIntegerVector<>::deserialize( + full_reader, pixie::DeserializationValidation::kFull), + std::invalid_argument); + EXPECT_EQ(full_reader.position(), 0u); +} + +TEST(IntegerVectorSerializationTest, + MonotoneWrapperRejectsPlainPackedArtifact) { + const std::array values = {std::uint64_t{1}, std::uint64_t{2}}; + const auto bytes = serialize_to_bytes(pixie::PackedIntegerVector<>(values)); + pixie::BinaryReader reader(bytes); + EXPECT_THROW((void)pixie::PackedMonotoneIntegerVector<>::deserialize(reader), + std::invalid_argument); + EXPECT_EQ(reader.position(), 0u); +} + +TEST(IntegerVectorSerializationTest, + RejectsMonotoneWrapperCorruptionTransactionally) { + const std::array values = {std::uint64_t{1}, std::uint64_t{2}}; + const pixie::PackedMonotoneIntegerVector<> original{ + pixie::PackedIntegerVector<>(values)}; + const auto serialized = serialize_to_bytes(original); + const auto expect_rejected = [](std::vector bytes) { + pixie::BinaryReader reader(bytes); + EXPECT_THROW( + (void)pixie::PackedMonotoneIntegerVector<>::deserialize(reader), + std::exception); + EXPECT_EQ(reader.position(), 0u); + }; + + auto bad_magic = serialized; + bad_magic[0] ^= std::byte{1}; + expect_rejected(std::move(bad_magic)); + auto bad_version = serialized; + overwrite_little_endian(std::span(bad_version), 8, + std::uint32_t{2}); + expect_rejected(std::move(bad_version)); + auto bad_reserved = serialized; + overwrite_little_endian(std::span(bad_reserved), 12, + std::uint32_t{1}); + expect_rejected(std::move(bad_reserved)); + auto bad_size = serialized; + overwrite_little_endian(std::span(bad_size), 16, + std::uint64_t{16}); + expect_rejected(std::move(bad_size)); + auto trailing = serialized; + trailing.resize(trailing.size() + sizeof(std::uint64_t)); + overwrite_little_endian(std::span(trailing), 16, + static_cast(trailing.size())); + expect_rejected(std::move(trailing)); +} + +TEST(IntegerVectorSerializationTest, + SerializersRejectUnalignedOffsetsBeforeWriting) { + const std::array values = {std::uint64_t{1}, std::uint64_t{2}}; + const pixie::PackedIntegerVector<> packed(values); + const pixie::PackedMonotoneIntegerVector<> monotone{ + pixie::PackedIntegerVector<>(values)}; + { + pixie::VectorOutputSink sink; + pixie::BinaryWriter writer(sink); + writer.write_u8(0); + EXPECT_THROW(packed.serialize(writer), std::invalid_argument); + EXPECT_EQ(writer.size_bytes(), 1u); + } + { + pixie::VectorOutputSink sink; + pixie::BinaryWriter writer(sink); + writer.write_u8(0); + EXPECT_THROW(monotone.serialize(writer), std::invalid_argument); + EXPECT_EQ(writer.size_bytes(), 1u); + } +} + +TEST(IntegerVectorSerializationTest, MappedViewRetainsMappedPayload) { + const std::vector values = {0, 1, 7, 31, 1000, 1000000}; + const pixie::PackedMonotoneIntegerVector<> original{ + pixie::PackedIntegerVector<>(values)}; + const auto path = std::filesystem::temp_directory_path() / + "pixie_integer_vector_mapped_test.bin"; + std::filesystem::remove(path); + { + pixie::io::FileOutputSink sink(path); + std::array staging{}; + pixie::BinaryWriter writer(sink, staging); + original.serialize(writer); + writer.finish(); + } + + pixie::io::MappedFile mapped(path); + pixie::BinaryReader reader(mapped.as_bytes()); + const auto view = pixie::PackedMonotoneIntegerVectorView<>::deserialize( + reader, pixie::DeserializationValidation::kFull); + std::filesystem::remove(path); + EXPECT_TRUE(reader.empty()); + check_monotone_queries(view, values); +} + +} // namespace diff --git a/src/tests/rank_select_tests.cpp b/src/tests/rank_select_tests.cpp index 80be93f..ac8a374 100644 --- a/src/tests/rank_select_tests.cpp +++ b/src/tests/rank_select_tests.cpp @@ -1,7 +1,7 @@ #include #include #include -#include +#include #include #include diff --git a/src/tests/rank_select_unittests.cc b/src/tests/rank_select_unittests.cc index c31724d..4547144 100644 --- a/src/tests/rank_select_unittests.cc +++ b/src/tests/rank_select_unittests.cc @@ -1,5 +1,5 @@ #include -#include +#include #include #include diff --git a/src/tests/rmq_tests.cpp b/src/tests/rmq_tests.cpp index 08ff6a9..26abbaf 100644 --- a/src/tests/rmq_tests.cpp +++ b/src/tests/rmq_tests.cpp @@ -1,10 +1,18 @@ #include #include #include -#include +#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 index 8c29bd7..79a45e0 100644 --- a/src/tests/storage_tests.cpp +++ b/src/tests/storage_tests.cpp @@ -1,6 +1,7 @@ #include #include -#include +#include +#include #include #include diff --git a/src/tests/test_rmm.cpp b/src/tests/test_rmm.cpp index f802076..3023da1 100644 --- a/src/tests/test_rmm.cpp +++ b/src/tests/test_rmm.cpp @@ -1,9 +1,14 @@ #include #include #include -#include +#include +#include #include +#ifdef SDSL_SUPPORT +#include +#endif + #include #include #include diff --git a/src/tests/tree_tests.cpp b/src/tests/tree_tests.cpp index b828ddc..3b6bc89 100644 --- a/src/tests/tree_tests.cpp +++ b/src/tests/tree_tests.cpp @@ -1,5 +1,7 @@ #include -#include +#include +#include +#include #include #include diff --git a/src/tests/wavelet_tree_tests.cpp b/src/tests/wavelet_tree_tests.cpp index abd384f..52d1cb1 100644 --- a/src/tests/wavelet_tree_tests.cpp +++ b/src/tests/wavelet_tree_tests.cpp @@ -2,7 +2,7 @@ #include #include #include -#include +#include #include #include