Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
36aaa7f
feat: native pure-Rust u64 FastPFOR codec
CommanderStorm Jul 17, 2026
9d76737
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jul 17, 2026
53e4c19
docs: de-vibe comments and use one sentence per line
CommanderStorm Jul 17, 2026
10ced00
refactor: one codec type per block size for u32 and u64
CommanderStorm Jul 17, 2026
989b9a3
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jul 17, 2026
b3d15e2
Update fuzz/fuzz_targets/fastpfor_u64.rs
CommanderStorm Jul 17, 2026
276df9f
refactor: share one width-generic page engine between u32 and u64
CommanderStorm Jul 17, 2026
91d0be5
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jul 17, 2026
3b5e457
Merge branch 'main' into feat/native-u64-fastpfor
CommanderStorm Jul 17, 2026
640866d
fix: satisfy nightly clippy and rustfmt for CI
CommanderStorm Jul 17, 2026
e17a413
Merge branch 'main' into feat/native-u64-fastpfor
nyurik Jul 17, 2026
620d824
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jul 17, 2026
c6045e0
wip
nyurik Jul 17, 2026
dcf64f7
wip
nyurik Jul 17, 2026
2a2e2da
optimize
nyurik Jul 18, 2026
b6f4da0
wip
nyurik Jul 18, 2026
84d4367
wip
nyurik Jul 18, 2026
9b3fbed
wip
nyurik Jul 18, 2026
f68089f
refactor more to per-bit files
nyurik Jul 18, 2026
2f9f7b4
Merge branch 'main' into feat/native-u64-fastpfor
nyurik Jul 18, 2026
1df5c8c
fix: allow inline_always on fast_pack/fast_unpack forwarders
CommanderStorm Jul 18, 2026
dadcb05
fix: repair broken rustdoc intra-doc links on FastPFor
CommanderStorm Jul 18, 2026
6290d3d
refactor: width-generic codec family, split u32/u64 by type
CommanderStorm Jul 18, 2026
b267b64
refactor: source generic integer ops from num_traits::PrimInt
CommanderStorm Jul 18, 2026
2f5fc2c
Apply suggestion from @CommanderStorm
CommanderStorm Jul 18, 2026
056725d
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jul 18, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,13 @@ cpp_portable = ["cpp"]
# Optimize FastPFOR for the current CPU.
cpp_native = ["cpp"]
cpp = ["dep:cmake", "dep:cxx", "dep:cxx-build"]
rust = ["dep:bytes"]
rust = ["dep:bytes", "dep:num-traits"]

[dependencies]
bytemuck = { version = "1.25.0", features = ["min_const_generics"] }
bytes = { version = "1.11", optional = true }
cxx = { version = "1.0.194", optional = true }
num-traits = { version = "0.2", optional = true }
thiserror = "2.0.18"

[build-dependencies]
Expand Down
46 changes: 35 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,12 +58,34 @@ codec.decode_blocks(&encoded, Some(u32::try_from(blocks.len() * 256).expect("blo
assert_eq!(decoded, input);
```

### 64-bit integers (`u64`)

The `FastPForWide128` / `FastPForWide256` codecs compress `u64` values.
They implement `AnyLenCodec` (with `Elem = u64`) for native use, and `BlockCodec64`
(`encode64` / `decode64`) for comparison against the C++ codecs.
The wire format is byte-compatible with the C++ `CppFastPFor128` / `CppFastPFor256` 64-bit paths.

```rust
use fastpfor::{AnyLenCodec, FastPForWide256};

let mut codec = FastPForWide256::default();
let input: Vec<u64> = (0..600).map(|i| i * 1_000_000_000).collect();

let mut encoded = Vec::new();
codec.encode(&input, &mut encoded).unwrap();

let mut decoded = Vec::new();
codec.decode(&encoded, &mut decoded, None).unwrap();

assert_eq!(decoded, input);
```

### C++ Wrapper (`cpp` feature)

Enable the `cpp` feature in `Cargo.toml`:

```toml
fastpfor = { version = "0.1", features = ["cpp"] }
fastpfor = { version = "0.9", features = ["cpp"] }
```

All C++ codecs implement the same `AnyLenCodec` trait (`encode` / `decode`), so
Expand All @@ -90,16 +112,18 @@ The `FASTPFOR_SIMD_MODE` environment variable (`portable` or `native`) can overr

### Rust (`rust` feature)

Rust block codecs require block-aligned input. `CompositeCodec` chains a block codec with a tail codec (e.g. `VariableByte`) to handle arbitrary-length input. `FastPFor256` and `FastPFor128` are type aliases for such composites.

| Codec | Description |
|--------------------|--------------------------------------------------------------|
| `FastPFor256` | `CompositeCodec` of `FastPForBlock256` + `VariableByte` |
| `FastPFor128` | `CompositeCodec` of `FastPForBlock128` + `VariableByte` |
| `VariableByte` | Variable-byte encoding, MSB is opposite to protobuf's varint |
| `JustCopy` | No compression; useful as a baseline |
| `FastPForBlock256` | `FastPFor` with 256-element blocks; block-aligned input only |
| `FastPForBlock128` | `FastPFor` with 128-element blocks; block-aligned input only |
Rust block codecs require block-aligned input. `CompositeCodec` chains a block codec with a tail codec (e.g. `VariableByte`) to handle arbitrary-length input. `FastPFor256`/`FastPFor128` (for `u32`) and `FastPForWide256`/`FastPForWide128` (for `u64`) are type aliases for such composites.

| Codec | Description |
|--------------------|-----------------------------------------------------------------|
| `FastPFor256` | `CompositeCodec` of `FastPForBlock256` + `VariableByte` (`u32`) |
| `FastPFor128` | `CompositeCodec` of `FastPForBlock128` + `VariableByte` (`u32`) |
| `FastPForWide256` | `CompositeCodec` of `FastPForBlockWide256` + `VariableByte` (`u64`) |
| `FastPForWide128` | `CompositeCodec` of `FastPForBlockWide128` + `VariableByte` (`u64`) |
| `VariableByte` | Variable-byte encoding, MSB is opposite to protobuf's varint |
| `JustCopy` | No compression; useful as a baseline |
| `FastPForBlock256` | `FastPFor` with 256-element `u32` blocks; block-aligned input only |
| `FastPForBlock128` | `FastPFor` with 128-element `u32` blocks; block-aligned input only |

### C++ (`cpp` feature)

Expand Down
6 changes: 6 additions & 0 deletions fuzz/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,9 @@ name = "compare_fastpfor_128"
path = "fuzz_targets/compare_fastpfor_128.rs"
test = false
doc = false

[[bin]]
name = "fastpfor_u64"
path = "fuzz_targets/fastpfor_u64.rs"
test = false
doc = false
59 changes: 59 additions & 0 deletions fuzz/fuzz_targets/fastpfor_u64.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
#![no_main]

use fastpfor::cpp::{CppFastPFor128, CppFastPFor256};
use fastpfor::{BlockCodec64, FastPForWide128, FastPForWide256};
use libfuzzer_sys::fuzz_target;

#[derive(arbitrary::Arbitrary, Debug)]
struct Input {
data: Vec<u64>,
use_256: bool,
}

fn check(rust: &mut impl BlockCodec64, cpp: &mut impl BlockCodec64, data: &[u64], name: &str) {
let mut rust_enc = Vec::new();
rust.encode64(data, &mut rust_enc)
.expect("Rust encode64 failed");

let mut cpp_enc = Vec::new();
cpp.encode64(data, &mut cpp_enc)
.expect("C++ encode64 failed");

assert_eq!(
rust_enc, cpp_enc,
"{name}: Rust and C++ encode64 bytes differ"
);

let mut rust_dec = Vec::new();
rust.decode64(&rust_enc, &mut rust_dec)
.expect("Rust decode64 of own output failed");
assert_eq!(rust_dec, data, "{name}: Rust roundtrip mismatch");

let mut cross = Vec::new();
rust.decode64(&cpp_enc, &mut cross)
.expect("Rust decode64 of C++ output failed");
assert_eq!(cross, data, "{name}: Rust could not decode C++ output");

let mut cpp_dec = Vec::new();
cpp.decode64(&rust_enc, &mut cpp_dec)
.expect("C++ decode64 of Rust output failed");
assert_eq!(cpp_dec, data, "{name}: C++ could not decode Rust output");
}

fuzz_target!(|input: Input| {
if input.use_256 {
check(
&mut FastPForWide256::default(),
&mut CppFastPFor256::default(),
&input.data,
"FastPForWide256",
);
} else {
check(
&mut FastPForWide128::default(),
&mut CppFastPFor128::default(),
&input.data,
"FastPForWide128",
);
}
});
49 changes: 30 additions & 19 deletions src/codec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ pub(crate) fn default_max_decoded_len(compressed_words: usize) -> usize {
/// #[derive(Default)]
/// struct MyCodec;
/// impl BlockCodec for MyCodec {
/// type Elem = u32;
/// type Block = [u32; 256];
/// fn encode_blocks(&mut self, blocks: &[[u32; 256]], out: &mut Vec<u32>)
/// -> FastPForResult<()> { todo!() }
Expand All @@ -36,21 +37,25 @@ pub(crate) fn default_max_decoded_len(compressed_words: usize) -> usize {
/// }
/// ```
pub trait BlockCodec: Default {
/// The fixed-size block type. Must be plain-old-data (`Pod`).
/// In practice this will be `[u32; 128]` or `[u32; 256]`.
/// The unpacked element type: [`u32`] or [`u64`].
/// The compressed stream is always `Vec<u32>`; only decoded values use this width.
type Elem: Pod;

/// The fixed-size block type, which must be plain-old-data (`Pod`).
/// In practice `[u32; 128]`, `[u32; 256]`, `[u64; 128]`, or `[u64; 256]`.
type Block: Pod;

/// Number of `u32` elements in one block.
/// Number of [`Elem`](BlockCodec::Elem) values in one block.
///
/// Equal to `size_of::<Self::Block>() / 4`. Use this when computing
/// element counts from block counts, e.g. `n_blocks * codec.elements_per_block()`.
/// Equal to `size_of::<Self::Block>() / size_of::<Self::Elem>()`.
/// Use this when computing element counts from block counts.
#[inline]
#[must_use]
fn size() -> usize
where
Self: Sized,
{
size_of::<Self::Block>() / size_of::<u32>()
size_of::<Self::Block>() / size_of::<Self::Elem>()
}

/// Compress a slice of complete, fixed-size blocks.
Expand All @@ -76,7 +81,7 @@ pub trait BlockCodec: Default {
&mut self,
input: &[u32],
expected_len: Option<u32>,
out: &mut Vec<u32>,
out: &mut Vec<Self::Elem>,
) -> FastPForResult<usize>;

/// Maximum decompressed element count for a given compressed input length.
Expand All @@ -93,13 +98,13 @@ pub trait BlockCodec: Default {

/// Codec that supports compressing 64-bit integers into a 32-bit word stream.
///
/// Only three C++ codecs implement this trait: `CppFastPFor128`,
/// `CppFastPFor256`, and `CppVarInt`. For simple use, call
/// `encode64` / `decode64` directly on the struct — no trait import required.
/// Implemented by the pure-Rust [`FastPForWide128`](crate::FastPForWide128) and
/// [`FastPForWide256`](crate::FastPForWide256) codecs.
/// With the `cpp` feature, `CppFastPFor128`, `CppFastPFor256`, and `CppVarInt` also implement it.
///
/// Import `BlockCodec64` only when writing generic code over multiple codecs
/// that support 64-bit compression.
#[cfg(feature = "cpp")]
/// This is a shared interface for cross-codec comparison; for native Rust use,
/// [`FastPForWide128`](crate::FastPForWide128) also implements [`AnyLenCodec`] with `Elem = u64`.
/// Import `BlockCodec64` only when writing generic code over several 64-bit codecs.
pub trait BlockCodec64 {
/// Compress 64-bit integers into a 32-bit word stream.
fn encode64(&mut self, input: &[u64], out: &mut Vec<u32>) -> FastPForResult<()>;
Expand All @@ -114,8 +119,12 @@ pub trait BlockCodec64 {
/// trait directly. Block-oriented codecs are wrapped in `CompositeCodec`
/// to produce an `AnyLenCodec`.
pub trait AnyLenCodec: Default {
/// Compress an arbitrary-length slice of `u32` values.
fn encode(&mut self, input: &[u32], out: &mut Vec<u32>) -> FastPForResult<()>;
/// The unpacked element type: [`u32`] or [`u64`].
/// The compressed stream is always `Vec<u32>`; only decoded values use this width.
type Elem;

/// Compress an arbitrary-length slice of [`Elem`](AnyLenCodec::Elem) values.
fn encode(&mut self, input: &[Self::Elem], out: &mut Vec<u32>) -> FastPForResult<()>;

/// Maximum decompressed element count for a given compressed input length.
/// Reject `expected_len` values exceeding this to avoid allocation from bad data.
Expand All @@ -140,7 +149,7 @@ pub trait AnyLenCodec: Default {
fn decode(
&mut self,
input: &[u32],
out: &mut Vec<u32>,
out: &mut Vec<Self::Elem>,
expected_len: Option<u32>,
) -> FastPForResult<()>;
}
Expand All @@ -164,9 +173,11 @@ pub trait AnyLenCodec: Default {
/// assert_eq!(remainder.len(), 88);
/// ```
#[must_use]
pub fn slice_to_blocks<Blocks: BlockCodec + Sized>(input: &[u32]) -> (&[Blocks::Block], &[u32]) {
let block_u32s = Blocks::size();
let aligned_down = (input.len() / block_u32s) * block_u32s;
pub fn slice_to_blocks<Blocks: BlockCodec + Sized>(
input: &[Blocks::Elem],
) -> (&[Blocks::Block], &[Blocks::Elem]) {
let block_elems = Blocks::size();
let aligned_down = (input.len() / block_elems) * block_elems;
let (aligned, remainder) = input.split_at(aligned_down);
let blocks: &[Blocks::Block] = cast_slice(aligned); // must not panic
(blocks, remainder)
Expand Down
2 changes: 2 additions & 0 deletions src/cpp/codecs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ macro_rules! implement_cpp_codecs {
}

impl AnyLenCodec for $name {
type Elem = u32;

fn encode(&mut self, input: &[u32], out: &mut Vec<u32>) -> FastPForResult<()> {
encode32_to_vec_ffi(&self.0, input, out)
}
Expand Down
7 changes: 0 additions & 7 deletions src/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,6 @@ pub fn greatest_multiple(value: u32, factor: u32) -> u32 {
value - value % factor
}

/// Returns the number of bits needed to represent `i`.
/// Returns 0 for input 0.
#[cfg_attr(feature = "cpp", allow(dead_code))]
pub fn bits(i: u32) -> usize {
32 - i.leading_zeros().as_usize()
}

pub trait AsUsize: Eq + Copy {
fn as_usize(self) -> usize;

Expand Down
7 changes: 3 additions & 4 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,7 @@ pub mod cpp;
pub(crate) mod rust;

mod codec;
#[cfg(feature = "cpp")]
pub use codec::BlockCodec64;
pub use codec::{AnyLenCodec, BlockCodec, slice_to_blocks};
pub use codec::{AnyLenCodec, BlockCodec, BlockCodec64, slice_to_blocks};

pub(crate) mod helpers;

Expand All @@ -31,7 +29,8 @@ pub use bytemuck::Pod;
#[cfg(feature = "rust")]
pub use rust::{
CompositeCodec, FastPFor, FastPFor128, FastPFor256, FastPForBlock128, FastPForBlock256,
JustCopy, VariableByte,
FastPForBlockWide128, FastPForBlockWide256, FastPForWide128, FastPForWide256, JustCopy,
VariableByte,
};

// `src/test_utils.rs` uses `fastpfor::...`; alias this crate for unit tests only.
Expand Down
19 changes: 13 additions & 6 deletions src/rust/composite.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,26 +40,33 @@ use crate::helpers::AsUsize;
/// codec.decode(&encoded, &mut decoded, None).unwrap();
/// assert_eq!(decoded, data);
/// ```
pub struct CompositeCodec<Blocks: BlockCodec, Tail: AnyLenCodec> {
#[derive(Debug)]
pub struct CompositeCodec<Blocks: BlockCodec, Tail: AnyLenCodec<Elem = Blocks::Elem>> {
block: Blocks,
tail: Tail,
}

impl<Blocks: BlockCodec, Tail: AnyLenCodec> Default for CompositeCodec<Blocks, Tail> {
impl<Blocks: BlockCodec, Tail: AnyLenCodec<Elem = Blocks::Elem>> Default
for CompositeCodec<Blocks, Tail>
{
fn default() -> Self {
Self::new(Blocks::default(), Tail::default())
}
}

impl<Blocks: BlockCodec, Tail: AnyLenCodec> CompositeCodec<Blocks, Tail> {
impl<Blocks: BlockCodec, Tail: AnyLenCodec<Elem = Blocks::Elem>> CompositeCodec<Blocks, Tail> {
/// Creates a new `CompositeCodec` from a block codec and a tail codec.
pub fn new(block: Blocks, tail: Tail) -> Self {
Self { block, tail }
}
}

impl<Blocks: BlockCodec, Tail: AnyLenCodec> AnyLenCodec for CompositeCodec<Blocks, Tail> {
fn encode(&mut self, input: &[u32], out: &mut Vec<u32>) -> FastPForResult<()> {
impl<Blocks: BlockCodec, Tail: AnyLenCodec<Elem = Blocks::Elem>> AnyLenCodec
for CompositeCodec<Blocks, Tail>
{
type Elem = Blocks::Elem;

fn encode(&mut self, input: &[Self::Elem], out: &mut Vec<u32>) -> FastPForResult<()> {
let (blocks, remainder) = slice_to_blocks::<Blocks>(input);
// C++ CompositeCodec: concatenate block + tail. Block codec writes length header (0 when empty).
self.block.encode_blocks(blocks, out)?;
Expand All @@ -70,7 +77,7 @@ impl<Blocks: BlockCodec, Tail: AnyLenCodec> AnyLenCodec for CompositeCodec<Block
fn decode(
&mut self,
input: &[u32],
out: &mut Vec<u32>,
out: &mut Vec<Self::Elem>,
expected_len: Option<u32>,
) -> FastPForResult<()> {
let start_len = out.len();
Expand Down
Loading
Loading