Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,27 @@ codec.decode_blocks(&encoded, Some(u32::try_from(blocks.len() * 256).expect("blo
assert_eq!(decoded, input);
```

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

`FastPForWide128` / `FastPForWide256` compress `u64` values via the `BlockCodec64`
trait. The wire format is byte-compatible with the C++ `CppFastPFor128` /
`CppFastPFor256` `encode64` / `decode64` paths.

```rust
use fastpfor::{BlockCodec64, 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.encode64(&input, &mut encoded).unwrap();

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

assert_eq!(decoded, input);
```

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

Enable the `cpp` feature in `Cargo.toml`:
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
60 changes: 60 additions & 0 deletions fuzz/fuzz_targets/fastpfor_u64.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
#![no_main]

//! Differential fuzz of the 64-bit FastPFOR codec.
//!
//! For any `u64` input, `FastPForWide` and the C++ codec must produce identical compressed bytes.
//! Every decoder must reproduce the original input.
//! Each side must also decode the other's output.

Comment thread
CommanderStorm marked this conversation as resolved.
Outdated
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",
);
}
});
10 changes: 4 additions & 6 deletions src/codec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,13 +93,11 @@ 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 [`FastPForWide`](crate::FastPForWide) codecs.
/// With the `cpp` feature, `CppFastPFor128`, `CppFastPFor256`, and `CppVarInt` also implement it.
/// For simple use, call `encode64` / `decode64` directly on the struct.
///
/// Import `BlockCodec64` only when writing generic code over multiple codecs
/// that support 64-bit compression.
#[cfg(feature = "cpp")]
/// 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 Down
3 changes: 1 addition & 2 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ pub mod cpp;
pub(crate) mod rust;

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

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

// `src/test_utils.rs` uses `fastpfor::...`; alias this crate for unit tests only.
Expand Down
116 changes: 116 additions & 0 deletions src/rust/integer_compression/bitpacking_wide.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
//! Generic scalar bit-packing for 64-bit values.
//!
//! Packs and unpacks groups of 32 values at any bit width `0..=64`.
//! The layout is a little-endian bitstream, matching the hand-unrolled 32-bit kernels in [`bitpacking`](super::bitpacking).
//! Value `j` occupies bits `[j*bit, (j+1)*bit)` of the concatenated stream.
//! Each call moves exactly `bit` `u32` words.

const fn low_mask(bit: u8) -> u64 {
if bit >= 64 {
u64::MAX
} else {
(1u64 << bit) - 1
}
}

/// Packs 32 values from `input[inpos..]` into `output[outpos..]` at `bit` bits each.
pub fn pack_wide(input: &[u64], inpos: usize, output: &mut [u32], outpos: usize, bit: u8) {
if bit == 0 {
return;
}
let mask = u128::from(low_mask(bit));
let mut acc: u128 = 0;
let mut filled: u32 = 0;
let mut out = outpos;
for j in 0..32 {
acc |= (u128::from(input[inpos + j]) & mask) << filled;
filled += u32::from(bit);
while filled >= 32 {
output[out] = acc as u32;
out += 1;
acc >>= 32;
filled -= 32;
}
}
}

/// Unpacks 32 values from `input[inpos..]` into `output[outpos..]` at `bit` bits each.
pub fn unpack_wide(input: &[u32], inpos: usize, output: &mut [u64], outpos: usize, bit: u8) {
if bit == 0 {
output[outpos..outpos + 32].fill(0);
return;
}
let mask = u128::from(low_mask(bit));
let mut acc: u128 = 0;
let mut avail: u32 = 0;
let mut inp = inpos;
for j in 0..32 {
while avail < u32::from(bit) {
acc |= u128::from(input[inp]) << avail;
inp += 1;
avail += 32;
}
output[outpos + j] = (acc & mask) as u64;
acc >>= u32::from(bit);
avail -= u32::from(bit);
}
}

#[cfg(test)]
mod tests {
use super::*;
use crate::rust::integer_compression::{bitpacking, bitunpacking};

#[test]
fn wide_matches_u32_kernels() {
let values32: [u32; 32] = std::array::from_fn(|i| (i as u32).wrapping_mul(2_654_435_761));

for bit in 1..=32u8 {
let mask = if bit == 32 {
u32::MAX
} else {
(1u32 << bit) - 1
};
let masked32: [u32; 32] = std::array::from_fn(|i| values32[i] & mask);
let masked64: [u64; 32] = std::array::from_fn(|i| u64::from(masked32[i]));

let mut out_ref = vec![0u32; bit as usize];
bitpacking::fast_pack(&masked32, 0, &mut out_ref, 0, bit);

let mut out_wide = vec![0u32; bit as usize];
pack_wide(&masked64, 0, &mut out_wide, 0, bit);

assert_eq!(out_ref, out_wide, "pack mismatch at bit={bit}");

let mut back_ref = vec![0u32; 32];
bitunpacking::fast_unpack(&out_ref, 0, &mut back_ref, 0, bit);
let mut back_wide = vec![0u64; 32];
unpack_wide(&out_wide, 0, &mut back_wide, 0, bit);

for i in 0..32 {
assert_eq!(
u64::from(back_ref[i]),
back_wide[i],
"unpack mismatch at bit={bit}"
);
}
}
}

#[test]
fn wide_roundtrip_all_widths() {
for bit in 0..=64u8 {
let mask = low_mask(bit);
let values: [u64; 32] =
std::array::from_fn(|i| (i as u64).wrapping_mul(0x9E37_79B9_7F4A_7C15) & mask);

let mut packed = vec![0u32; bit as usize];
pack_wide(&values, 0, &mut packed, 0, bit);

let mut back = vec![0u64; 32];
unpack_wide(&packed, 0, &mut back, 0, bit);

assert_eq!(values.to_vec(), back, "roundtrip mismatch at bit={bit}");
}
}
}
Loading
Loading