You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: README.md
+32-69Lines changed: 32 additions & 69 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -11,26 +11,20 @@ A Rust library that provides space-efficient, in-memory representations for inte
11
11
12
12
The library is designed to reduce the memory footprint of standard [`std::vec::Vec`] collections of integers while retaining performant access patterns.
13
13
14
-
## Core Structures: `FixedVec` and `IntVec`
14
+
## Core Structures: [`FixedVec`] and [`IntVec`]
15
15
16
16
The library provides two distinct vector types, each based on a different encoding principle. Choosing the right one depends on your specific use case, performance requirements, and data characteristics.
17
17
18
-
### `FixedVec`: Fixed-Width Encoding
18
+
### [`FixedVec`]: Fixed-Width Encoding
19
19
20
20
Implements a vector where every integer occupies the same, predetermined number of bits.
21
21
22
22
***Key Features**:
23
-
***O(1) Random Access**: The memory location of any element is determined by a direct bit-offset calculation, resulting in minimal-overhead access. With low bit widths (e.g., 8, 16, 32), `FixedVec` can be 2-3x faster than `std::vec::Vec` for random access.
23
+
***O(1) Random Access**: The memory location of any element is determined by a direct bit-offset calculation, resulting in minimal-overhead access. With low bit widths (e.g., 8, 16, 32), [`FixedVec`] can be 2-3x faster than [`std::vec::Vec`] for random access.
24
24
***Mutability**: Supports in-place modifications after creation through an API similar to [`std::vec::Vec`] (e.g., `push`, `set`, `pop`).
25
25
***Atomic Operations**: Provides [`AtomicFixedVec`], a thread-safe variant that supports atomic read-modify-write operations for concurrent environments.
26
26
27
-
***Use Cases**:
28
-
* Data with a uniform or near-uniform distribution where all values fit within a known bit width.
29
-
* Applications where low-latency random access is the primary performance requirement.
30
-
* Scenarios requiring in-place vector modification or concurrent atomic updates.
31
-
32
-
33
-
### `IntVec`: Variable-Width Encoding
27
+
### [`IntVec`]: Variable-Width Encoding
34
28
35
29
Implements a vector using variable-length instantaneous codes (e.g., Gamma, Delta, Rice, Zeta) to represent each integer.
36
30
@@ -39,32 +33,17 @@ Implements a vector using variable-length instantaneous codes (e.g., Gamma, Delt
39
33
***Automatic Codec Selection**: Can analyze the data to select the most effective compression codec automatically.
40
34
***Amortized O(1) Random Access**: Enables fast random access by sampling the bit positions of elements at a configurable interval (`k`).
41
35
42
-
***Use Cases**:
43
-
* Applications where minimizing memory usage is the primary goal.
44
-
* Read-only datasets with skewed distributions.
45
-
* Large-scale data processing where the vector is built once and read many times.
The following examples demonstrate the primary use cases for [`FixedVec`] and [`IntVec`]. All common types and traits are available through the [`prelude`].
43
+
The following examples shows some use cases for [`FixedVec`] and [`IntVec`]. All common types and traits are available through the [`prelude`].
64
44
65
-
### Example: `FixedVec` for Uniform Data and Mutable Access
45
+
### Example: [`FixedVec`] for Uniform Data and Mutable Access
66
46
67
-
Use [`FixedVec`] when data is uniformly distributed or when you require in-place modification or atomic operations.
[`IntVec`] is the optimal choice when data that is not uniformly distributed and we want to minimize memory usage. It uses variable-length codes to represent integers, allowing for significant space savings compared to fixed-width encodings.
95
+
[`IntVec`] is the optimal choice when data that is not uniformly distributed and we want to minimize memory usage. It uses variable-length codes to represent integers.
119
96
120
97
The compression strategy is controlled by the [`VariableCodecSpec`] enum, passed to the builder.
121
98
@@ -126,19 +103,19 @@ For most use cases, the recommended strategy is [`VariableCodecSpec::Auto`], whi
126
103
|`VariableCodecSpec` Variant | Description & Encoding Strategy | Optimal Data Distribution |
127
104
| :--- | :--- | :--- |
128
105
|**`Auto`**|**Recommended default.** Analyzes the data to choose the best variable-length code, balancing build time and compression ratio. | Agnostic; adapts to the input data. |
129
-
|`Gamma` (γ) | A universal, parameter-free code. Encodes `n` using the unary code of ⌊log₂(*n*+1)⌋, followed by the remaining bits of `n`+1. | Implied distribution is ≈ 1/(2*x*²). Optimal for data skewed towards small non-negative integers. |
130
-
|`Delta` (δ) | A universal, parameter-free code. Encodes `n` using the γ code of ⌊log₂(*n*+1)⌋, making it more efficient than γ for larger values. | Implied distribution is ≈ 1/(2*x*(log *x*)²). |
106
+
|`Gamma` (γ) | A universal, parameter-free code. Encodes `n` using the unary code of log₂(*n*+1), followed by the remaining bits of `n`+1. | Implied distribution is ≈ 1/(2*x*²). Optimal for data skewed towards small non-negative integers. |
107
+
|`Delta` (δ) | A universal, parameter-free code. Encodes `n` using the γ code of log₂(*n*+1), making it more efficient than γ for larger values. | Implied distribution is ≈ 1/(2*x*(log *x*)²). |
131
108
|`Rice`| A fast, tunable version of Golomb codes where the parameter *b* must be a power of two. Encodes `n` by splitting it into a quotient (stored in unary) and a remainder (stored in binary). | Geometric distributions. |
132
109
|`Golomb`| A tunable code, more general than Rice. Encodes `n` by splitting it into a quotient (stored in unary) and a remainder (stored using a minimal binary code). | Geometric distributions. Implied distribution is ≈ 1/*r*ˣ. |
133
-
|`Zeta` (ζ) | A tunable code for power-law data. Encodes `n` based on ⌊⌊log₂(*n*+1)⌋/*k*⌋ in unary, followed by a minimal binary code for the remainder. | Power-law distributions (e.g., word frequencies, node degrees). Implied distribution is ≈ 1/*x*<sup>1+1/*k*</sup>. |
110
+
|`Zeta` (ζ) | A tunable code for power-law data. Encodes `n` based on log₂(*n*+1)/*k* in unary, followed by a minimal binary code for the remainder. | Power-law distributions (e.g., word frequencies, node degrees). Implied distribution is ≈ 1/*x*<sup>1+1/*k*</sup>. |
134
111
|`VByteLe`/`Be`| A byte-aligned code that uses a continuation bit to store integers in a variable number of bytes. Fast to decode. The big-endian variant is lexicographical. | Implied distribution is ≈ 1/*x*<sup>8/7</sup>. Good for general-purpose integer data. |
135
112
|`Omega` (ω) | A universal, parameter-free code that recursively encodes the length of the binary representation of `n`. | Implied distribution is approximately 1/*x*. Compact for very large numbers. |
136
113
|`Unary`| The simplest code. Encodes `n` as `n` zero-bits followed by a one-bit. | Geometric distributions with a very high probability of small values (e.g., boolean flags). |
137
114
|`Explicit`| An escape hatch to use any code from the [`dsi-bitstream::codes::Codes`][dsi-bitstream-codes] enum. | Advanced use cases requiring specific, unlisted codes. |
### Automatic Selection with `VariableCodecSpec::Auto`
118
+
### Automatic Selection with [`VariableCodecSpec::Auto`]
142
119
143
120
The `Auto` strategy removes the guesswork from codec selection. During the build phase, it analyzes the input data and selects the codec that offers the best compression ratio. This introduces a (non negligible) one-time cost for the analysis at construction time. Use the `Auto` codec when you want to create an [`IntVec`] once and read it many times, as the amortized cost of the analysis is negligible compared to the space savings and the performance of subsequent reads.
144
121
@@ -153,7 +130,7 @@ If you need to create multiple [`IntVec`] instances at run-time, consider using
The access strategy for a compressed [`IntVec`] has significant performance implications. The library provides several methods, each optimized for a specific access pattern. Using the appropriate method is key to achieving high throughput.
159
136
@@ -162,7 +139,8 @@ The access strategy for a compressed [`IntVec`] has significant performance impl
162
139
For retrieving a batch of elements from a slice of indices.
163
140
164
141
***Mechanism**: This method sorts the provided indices to transform a random access pattern into a single, monotonic scan over the compressed data. This approach minimizes expensive bitstream seek operations and leverages data locality.
165
-
***Use Case**: The preferred method for any batch lookup when all indices are known and can be stored in a slice.
142
+
143
+
This should be your preferred method for any batch lookup when all indices are known and can be stored in a slice.
166
144
167
145
```rust
168
146
usecompressed_intvec::prelude::*;
@@ -171,7 +149,7 @@ use rand::Rng; // For random number generation
### Dynamic Lookups: `IntVecReader` and `IntVecSeqReader`
207
186
208
-
For dynamic or interactive scenarios where lookup indices are not known in advance.
187
+
There are interactive scenarios where lookup indices are not known in advance. The library provides two reader types to handle such cases:
209
188
210
189
#### [`IntVecReader`]: Stateless Random Access
211
190
212
191
A **stateless** reader for efficient, repeated random lookups.
213
192
214
193
***Mechanism**: Amortizes the setup cost of the bitstream reader across multiple calls. Each `get` operation performs an independent seek from the nearest sample point.
215
-
***Use Case**: Optimal for sparse, unpredictable access patterns where there is no locality between consecutive lookups.
194
+
195
+
Optimal for sparse, unpredictable access patterns where there is no locality between consecutive lookups.
A **stateful** reader optimized for access patterns with sequential locality.
232
212
233
213
***Mechanism**: Maintains an internal cursor. If a requested index is forward and within the same sample block, it decodes from the last position, avoiding a full seek.
234
-
***Use Case**: Optimal for iterating through sorted or clustered indices where consecutive lookups are near each other.
214
+
215
+
Optimal for iterating through sorted or clustered indices where consecutive lookups are near each other.
For concurrent applications, the library provides [`AtomicFixedVec`], a thread-safe variant of [`FixedVec`]. It supports atomic read-modify-write (RMW) operations, enabling safe and efficient manipulation of shared integer data across multiple threads.
238
+
For concurrent applications, the library provides [`AtomicFixedVec`], a thread-safe variant of [`FixedVec`]. It supports atomic read-modify-write (RMW) operations, enabling safe and efficient manipulation of shared integer data across multiple threads. The atomicity guarantees depend on the element's bit width and its alignment within the underlying `u64` storage words.
258
239
259
-
***Mechanism**: The atomicity guarantees depend on the element's bit width and its alignment within the underlying `u64` storage words.
260
-
***Lock-Free Path**: When an element is fully contained within a single `u64` word (common for power-of-two bit widths), operations are performed using lock-free atomic hardware instructions. Performance is optimal as no locks are involved.
261
-
***Locked Path**: When an element's bits span across the boundary of two `u64` words, operations are protected by a fine-grained mutex from a striped lock pool. This ensures atomicity for the two-word update without resorting to a global lock.
240
+
***Lock-Free Path**: When an element is fully contained within a single `u64` word (guaranteed for power-of-two bit widths), operations are performed using lock-free atomic instructions. Performance here is optimal as no locks are involved.
241
+
***Locked Path**: When an element's bits span across the boundary of two `u64` words (common for non-power-of-two bit widths), operations are protected by a fine-grained mutex from a striped lock pool. This ensures atomicity for the two-word update without resorting to a global lock.
262
242
263
-
***Use Case**: Ideal for shared counters, parallel data processing, and any scenario requiring multiple threads to read from and write to the same integer vector concurrently. For write-heavy workloads, configuring the bit width to a power of two (e.g., 8, 16, 32) is recommended to ensure all operations remain on the lock-free path.
243
+
Ideal for shared counters, parallel data processing, and any scenario requiring multiple threads to read from and write to the same integer vector concurrently. For write-heavy workloads, configuring the bit width to a power of two (e.g., 8, 16, 32) is recommended to ensure all operations remain on the lock-free path.
// Create an IntVec with a generic Gamma encoding.
306
+
// Create an IntVec with Gamma encoding.
327
307
letgamma_intvec=LEIntVec::builder()
328
308
.codec(VariableCodecSpec::Gamma)
329
309
.build(&data)
@@ -355,12 +335,12 @@ fn main() {
355
335
356
336
The output displays memory breakdown for a 1,000,000-element vector of `u64` integers, uniformly distributed between 0 and 2<sup>20</sup>
357
337
358
-
-Standard `Vec<u64>`: 80.02 kB (8 bytes per element)
338
+
-`Vec<u64>`: 80.02 kB (8 bytes per element)
359
339
-[`IntVec`] with Gamma encoding: 47.10 kB (41% reduction)
360
340
-[`IntVec`] with Auto codec selection: 28.33 kB (65% reduction, selected Zeta k=10)
361
341
-[`FixedVec`] with minimal bit width: 25.06 kB (69% reduction, 20 bits per element)
362
342
363
-
The memory analysis reveals the internal structure of each data type, including storage overhead for metadata, sampling structures, and encoding parameters.
343
+
The memory analysis also shows the internal structure of each data type, including storage overhead for metadata, sampling structures, and encoding parameters.
364
344
365
345
```text
366
346
Size of the uncompressed Vec<u64>:
@@ -405,23 +385,6 @@ Size of the FixedVec with minimal bit width:
405
385
0 B 0.00% ╰╴_phantom
406
386
```
407
387
408
-
## Cargo Features
409
-
410
-
The library's functionality can be customized through the following Cargo features.
411
-
412
-
### `parallel` (Default Feature)
413
-
414
-
Enables parallel operations using the [Rayon] crate.Provides `par_iter` and `par_get_many` methods on [`FixedVec`] and [`IntVec`], as well as `par_iter` and `par_iter_mut` on [`AtomicFixedVec`].
415
-
416
-
This feature is enabled by default, as it significantly improves performance for batch operations on large vectors.
417
-
418
-
### `serde`
419
-
420
-
Enables serialization and deserialization for [`FixedVec`], [`AtomicFixedVec`], and [`IntVec`] by implementing the `Serialize` and `Deserialize` traits from the [Serde] framework.
@@ -436,7 +399,7 @@ The library includes benchmarks for both [`FixedVec`] and [`IntVec`]. It also te
436
399
cargo bench
437
400
```
438
401
439
-
The benchmarks measure the performance of random access, batch access, iter_access, and memory usage for various data distributions and vector sizes. For a visual representation of the random_access performance, you can then run the following command:
402
+
The benchmarks measure the performance of random access, batch access, iter_access, and memory usage for various data distributions and vector sizes. For a visual representation of the random access performance, run the following command:
0 commit comments