Skip to content

Commit 36aaa7f

Browse files
feat: native pure-Rust u64 FastPFOR codec
Adds a 64-bit (`u64`) FastPFOR codec to the pure-Rust side, which was previously 32-bit only. `FastPForWide128`/`FastPForWide256` compress `u64` values via the (now un-gated) `BlockCodec64` trait: FastPFOR-packed aligned blocks plus a variable-byte tail for the sub-block remainder. The wire format is byte-identical to the C++ `CppFastPFor128`/`CppFastPFor256` `encode64`/`decode64` paths, verified by parity unit tests and a 30-minute differential fuzz run (13.8M executions, no crashes). - bitpacking_wide: generic scalar packer for `u64` at widths 0..=64, whose bit-ordering is cross-checked against the proven u32 kernels. - fastpfor64: `FastPForWide<N>` with 65-entry exception tables, a 2-word exception bitmap, and u64 exception values. - Un-gate `BlockCodec64` so pure-Rust builds get 64-bit support. - Add the `fastpfor_u64` differential fuzz target and a README example. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent c81f34e commit 36aaa7f

10 files changed

Lines changed: 877 additions & 12 deletions

File tree

README.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,27 @@ codec.decode_blocks(&encoded, Some(u32::try_from(blocks.len() * 256).expect("blo
5858
assert_eq!(decoded, input);
5959
```
6060

61+
### 64-bit integers (`u64`)
62+
63+
`FastPForWide128` / `FastPForWide256` compress `u64` values via the `BlockCodec64`
64+
trait. The wire format is byte-compatible with the C++ `CppFastPFor128` /
65+
`CppFastPFor256` `encode64` / `decode64` paths.
66+
67+
```rust
68+
use fastpfor::{BlockCodec64, FastPForWide256};
69+
70+
let mut codec = FastPForWide256::default();
71+
let input: Vec<u64> = (0..600).map(|i| i * 1_000_000_000).collect();
72+
73+
let mut encoded = Vec::new();
74+
codec.encode64(&input, &mut encoded).unwrap();
75+
76+
let mut decoded = Vec::new();
77+
codec.decode64(&encoded, &mut decoded).unwrap();
78+
79+
assert_eq!(decoded, input);
80+
```
81+
6182
### C++ Wrapper (`cpp` feature)
6283

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

fuzz/Cargo.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,3 +62,9 @@ name = "compare_fastpfor_128"
6262
path = "fuzz_targets/compare_fastpfor_128.rs"
6363
test = false
6464
doc = false
65+
66+
[[bin]]
67+
name = "fastpfor_u64"
68+
path = "fuzz_targets/fastpfor_u64.rs"
69+
test = false
70+
doc = false

fuzz/fuzz_targets/fastpfor_u64.rs

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
#![no_main]
2+
3+
//! Differential fuzz of the 64-bit FastPFOR codec.
4+
//!
5+
//! For arbitrary `u64` input, the pure-Rust `FastPForWide` and the C++
6+
//! `CppFastPFor128` / `CppFastPFor256` must produce bit-identical compressed
7+
//! output, and every decoder must reproduce the original input — including
8+
//! decoding the other implementation's bytes.
9+
10+
use fastpfor::cpp::{CppFastPFor128, CppFastPFor256};
11+
use fastpfor::{BlockCodec64, FastPForWide128, FastPForWide256};
12+
use libfuzzer_sys::fuzz_target;
13+
14+
#[derive(arbitrary::Arbitrary, Debug)]
15+
struct Input {
16+
data: Vec<u64>,
17+
use_256: bool,
18+
}
19+
20+
fn check(rust: &mut impl BlockCodec64, cpp: &mut impl BlockCodec64, data: &[u64], name: &str) {
21+
let mut rust_enc = Vec::new();
22+
rust.encode64(data, &mut rust_enc).expect("Rust encode64 failed");
23+
24+
let mut cpp_enc = Vec::new();
25+
cpp.encode64(data, &mut cpp_enc).expect("C++ encode64 failed");
26+
27+
assert_eq!(rust_enc, cpp_enc, "{name}: Rust and C++ encode64 bytes differ");
28+
29+
let mut rust_dec = Vec::new();
30+
rust.decode64(&rust_enc, &mut rust_dec)
31+
.expect("Rust decode64 of own output failed");
32+
assert_eq!(rust_dec, data, "{name}: Rust roundtrip mismatch");
33+
34+
let mut cross = Vec::new();
35+
rust.decode64(&cpp_enc, &mut cross)
36+
.expect("Rust decode64 of C++ output failed");
37+
assert_eq!(cross, data, "{name}: Rust could not decode C++ output");
38+
39+
let mut cpp_dec = Vec::new();
40+
cpp.decode64(&rust_enc, &mut cpp_dec)
41+
.expect("C++ decode64 of Rust output failed");
42+
assert_eq!(cpp_dec, data, "{name}: C++ could not decode Rust output");
43+
}
44+
45+
fuzz_target!(|input: Input| {
46+
if input.use_256 {
47+
check(
48+
&mut FastPForWide256::default(),
49+
&mut CppFastPFor256::default(),
50+
&input.data,
51+
"FastPForWide256",
52+
);
53+
} else {
54+
check(
55+
&mut FastPForWide128::default(),
56+
&mut CppFastPFor128::default(),
57+
&input.data,
58+
"FastPForWide128",
59+
);
60+
}
61+
});

src/codec.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -93,13 +93,13 @@ pub trait BlockCodec: Default {
9393

9494
/// Codec that supports compressing 64-bit integers into a 32-bit word stream.
9595
///
96-
/// Only three C++ codecs implement this trait: `CppFastPFor128`,
97-
/// `CppFastPFor256`, and `CppVarInt`. For simple use, call
98-
/// `encode64` / `decode64` directly on the struct — no trait import required.
96+
/// Implemented by the pure-Rust [`FastPForWide`](crate::FastPForWide) codecs and,
97+
/// with the `cpp` feature, by `CppFastPFor128`, `CppFastPFor256`, and `CppVarInt`.
98+
/// For simple use, call `encode64` / `decode64` directly on the struct — no trait
99+
/// import required.
99100
///
100101
/// Import `BlockCodec64` only when writing generic code over multiple codecs
101102
/// that support 64-bit compression.
102-
#[cfg(feature = "cpp")]
103103
pub trait BlockCodec64 {
104104
/// Compress 64-bit integers into a 32-bit word stream.
105105
fn encode64(&mut self, input: &[u64], out: &mut Vec<u32>) -> FastPForResult<()>;

src/lib.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ pub mod cpp;
1818
pub(crate) mod rust;
1919

2020
mod codec;
21-
#[cfg(feature = "cpp")]
2221
pub use codec::BlockCodec64;
2322
pub use codec::{AnyLenCodec, BlockCodec, slice_to_blocks};
2423

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

3736
// `src/test_utils.rs` uses `fastpfor::...`; alias this crate for unit tests only.
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
//! Generic scalar bit-packing for 64-bit values.
2+
//!
3+
//! Packs and unpacks groups of 32 values at any bit width `0..=64` using the same
4+
//! little-endian bitstream layout as the hand-unrolled 32-bit kernels in
5+
//! [`bitpacking`](super::bitpacking): value `j` occupies bits `[j*bit, (j+1)*bit)`
6+
//! of the concatenated stream. Each call moves exactly `bit` `u32` words.
7+
8+
const fn low_mask(bit: u8) -> u64 {
9+
if bit >= 64 { u64::MAX } else { (1u64 << bit) - 1 }
10+
}
11+
12+
/// Packs 32 values from `input[inpos..]` into `output[outpos..]` at `bit` bits each.
13+
pub fn pack_wide(input: &[u64], inpos: usize, output: &mut [u32], outpos: usize, bit: u8) {
14+
if bit == 0 {
15+
return;
16+
}
17+
let mask = u128::from(low_mask(bit));
18+
let mut acc: u128 = 0;
19+
let mut filled: u32 = 0;
20+
let mut out = outpos;
21+
for j in 0..32 {
22+
acc |= (u128::from(input[inpos + j]) & mask) << filled;
23+
filled += u32::from(bit);
24+
while filled >= 32 {
25+
output[out] = acc as u32;
26+
out += 1;
27+
acc >>= 32;
28+
filled -= 32;
29+
}
30+
}
31+
}
32+
33+
/// Unpacks 32 values from `input[inpos..]` into `output[outpos..]` at `bit` bits each.
34+
pub fn unpack_wide(input: &[u32], inpos: usize, output: &mut [u64], outpos: usize, bit: u8) {
35+
if bit == 0 {
36+
output[outpos..outpos + 32].fill(0);
37+
return;
38+
}
39+
let mask = u128::from(low_mask(bit));
40+
let mut acc: u128 = 0;
41+
let mut avail: u32 = 0;
42+
let mut inp = inpos;
43+
for j in 0..32 {
44+
while avail < u32::from(bit) {
45+
acc |= u128::from(input[inp]) << avail;
46+
inp += 1;
47+
avail += 32;
48+
}
49+
output[outpos + j] = (acc & mask) as u64;
50+
acc >>= u32::from(bit);
51+
avail -= u32::from(bit);
52+
}
53+
}
54+
55+
#[cfg(test)]
56+
mod tests {
57+
use super::*;
58+
use crate::rust::integer_compression::{bitpacking, bitunpacking};
59+
60+
/// The wide packer must produce byte-identical output to the proven u32 kernels
61+
/// for every width `1..=32`, validating its bit ordering without needing C++.
62+
#[test]
63+
fn wide_matches_u32_kernels() {
64+
let values32: [u32; 32] = std::array::from_fn(|i| (i as u32).wrapping_mul(2_654_435_761));
65+
66+
for bit in 1..=32u8 {
67+
let mask = if bit == 32 { u32::MAX } else { (1u32 << bit) - 1 };
68+
let masked32: [u32; 32] = std::array::from_fn(|i| values32[i] & mask);
69+
let masked64: [u64; 32] = std::array::from_fn(|i| u64::from(masked32[i]));
70+
71+
let mut out_ref = vec![0u32; bit as usize];
72+
bitpacking::fast_pack(&masked32, 0, &mut out_ref, 0, bit);
73+
74+
let mut out_wide = vec![0u32; bit as usize];
75+
pack_wide(&masked64, 0, &mut out_wide, 0, bit);
76+
77+
assert_eq!(out_ref, out_wide, "pack mismatch at bit={bit}");
78+
79+
let mut back_ref = vec![0u32; 32];
80+
bitunpacking::fast_unpack(&out_ref, 0, &mut back_ref, 0, bit);
81+
let mut back_wide = vec![0u64; 32];
82+
unpack_wide(&out_wide, 0, &mut back_wide, 0, bit);
83+
84+
for i in 0..32 {
85+
assert_eq!(u64::from(back_ref[i]), back_wide[i], "unpack mismatch at bit={bit}");
86+
}
87+
}
88+
}
89+
90+
#[test]
91+
fn wide_roundtrip_all_widths() {
92+
for bit in 0..=64u8 {
93+
let mask = low_mask(bit);
94+
let values: [u64; 32] =
95+
std::array::from_fn(|i| (i as u64).wrapping_mul(0x9E37_79B9_7F4A_7C15) & mask);
96+
97+
let mut packed = vec![0u32; bit as usize];
98+
pack_wide(&values, 0, &mut packed, 0, bit);
99+
100+
let mut back = vec![0u64; 32];
101+
unpack_wide(&packed, 0, &mut back, 0, bit);
102+
103+
assert_eq!(values.to_vec(), back, "roundtrip mismatch at bit={bit}");
104+
}
105+
}
106+
}

0 commit comments

Comments
 (0)