Skip to content

Commit f070230

Browse files
committed
Optimize encoding incompressible u8.
1 parent 45f8103 commit f070230

5 files changed

Lines changed: 123 additions & 86 deletions

File tree

‎src/derive/vec.rs‎

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -363,7 +363,7 @@ impl<'a, T: Decode<'a>> Decoder<'a, VecDeque<T>> for VecDecoder<'a, T> {
363363
}
364364

365365
#[cfg(test)]
366-
mod test {
366+
mod tests {
367367
use alloc::collections::*;
368368
use alloc::vec::Vec;
369369

@@ -386,10 +386,24 @@ mod test {
386386
type T = BinaryHeap<u8>;
387387
let data: T = bench_data();
388388
let encoded = crate::encode(&data);
389+
390+
let mut buffer = crate::Buffer::new();
389391
b.iter(|| {
390-
let decoded: T = crate::decode::<T>(&encoded).unwrap();
392+
let decoded: T = buffer.decode::<T>(&encoded).unwrap();
391393
debug_assert!(data.iter().eq(decoded.iter()));
392394
decoded
393395
})
394396
}
395397
}
398+
399+
#[cfg(test)]
400+
mod tests2 {
401+
type T = [[u8; 32]; 32];
402+
fn bench_data() -> Vec<Vec<T>> {
403+
crate::random_data(1000)
404+
.into_iter()
405+
.map(|t: T| if t[0][0] & 1 == 0 { vec![t] } else { vec![] })
406+
.collect()
407+
}
408+
crate::bench_encode_decode!(vec_zero_or_one_large_array_vec: Vec<Vec<T>>);
409+
}

‎src/lib.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ mod length;
2929
mod nightly;
3030
mod pack;
3131
mod pack_ints;
32+
mod pack_shared;
3233
mod str;
3334
mod u8_char;
3435

‎src/pack.rs‎

Lines changed: 19 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use crate::consume::{consume_byte, consume_byte_arrays, consume_bytes};
33
use crate::error::err;
44
use crate::fast::CowSlice;
55
use crate::pack_ints::{Int, SizedInt};
6+
use crate::pack_shared::PackingTrait;
67
use alloc::vec::Vec;
78

89
/// Possible states per byte in descending order. Each packed byte will use `log2(states)` bits.
@@ -17,8 +18,9 @@ enum Packing {
1718
_2,
1819
}
1920

20-
impl Packing {
21-
fn new(max: u8) -> Self {
21+
impl PackingTrait for Packing {
22+
fn new<T: crate::pack_ints::SizedUInt>(max: T) -> Self {
23+
let max: u8 = bytemuck::must_cast(max);
2224
match max {
2325
// We could encode max 0 as nothing, but that could allocate unbounded memory when decoding.
2426
0..=1 => Self::_2,
@@ -30,12 +32,16 @@ impl Packing {
3032
}
3133
}
3234

33-
fn write(self, out: &mut Vec<u8>, offset_by_min: bool) {
35+
fn write<T: crate::pack_ints::SizedUInt>(self, out: &mut Vec<u8>, offset_by_min: bool) {
36+
// pack_ints::Packing needs generics, we only use this on u8 here.
37+
assert_eq!(std::mem::size_of::<T>(), 1);
3438
// Encoded in such a way such that 0 is `Self::_256` and higher numbers are smaller packing.
3539
// Also makes `Self::_256` with offset_by_min = true is unrepresentable.
3640
out.push(self as u8 * 2 - offset_by_min as u8);
3741
}
42+
}
3843

44+
impl Packing {
3945
fn read(input: &mut &[u8]) -> Result<(Self, bool)> {
4046
let v = consume_byte(input)?;
4147
let p_u8 = crate::nightly::div_ceil_u8(v, 2);
@@ -93,44 +99,26 @@ pub fn pack_bytes<T: Byte>(bytes: &mut [T], out: &mut Vec<u8>) {
9399
out.extend_from_slice(bytemuck::must_cast_slice(bytes));
94100
return;
95101
}
96-
let (min, max) = crate::pack_ints::minmax(bytes);
97-
98-
// i8 packs as u8 if positive.
99-
let basic_packing = if min >= T::default() {
100-
Packing::new(bytemuck::must_cast(max))
101-
} else {
102-
Packing::_256 // Any negative i8 as u8 is > 15 and can't be packed without offset_packing.
103-
};
104102

105-
// u8::wrapping_sub == i8::wrapping_sub, so we can use u8s from here onward.
106-
let min: u8 = bytemuck::must_cast(min);
107-
let max: u8 = bytemuck::must_cast(max);
103+
let (basic_packing, min_max) =
104+
crate::pack_shared::basic_packing_and_signed_min_max_cast_to_unsigned(bytes, out);
108105
let bytes: &mut [u8] = bytemuck::must_cast_slice_mut(bytes);
109-
pack_bytes_unsigned(bytes, out, basic_packing, min, max);
106+
107+
// <T as SizedInt>::Unsigned for T: Byte is always u8, but we can't prove
108+
// that here, so we perform this cast which doesn't change the underlying type.
109+
let min_max = min_max.map(|(min, max)| (bytemuck::must_cast(min), bytemuck::must_cast(max)));
110+
111+
pack_bytes_unsigned(bytes, out, basic_packing, min_max);
110112
}
111113

112114
/// [`pack_bytes`] but after i8s have been cast to u8s.
113115
fn pack_bytes_unsigned(
114116
bytes: &mut [u8],
115117
out: &mut Vec<u8>,
116118
basic_packing: Packing,
117-
min: u8,
118-
max: u8,
119+
min_max: Option<(u8, u8)>,
119120
) {
120-
// If subtracting min from all bytes results in a better packing do it, otherwise don't bother.
121-
let offset_packing = Packing::new(max.wrapping_sub(min));
122-
let p = if offset_packing > basic_packing && bytes.len() > 5 {
123-
for b in bytes.iter_mut() {
124-
*b = b.wrapping_sub(min);
125-
}
126-
offset_packing.write(out, true);
127-
out.push(min);
128-
offset_packing
129-
} else {
130-
basic_packing.write(out, false);
131-
basic_packing
132-
};
133-
121+
let p = crate::pack_shared::offset_packing(bytes, out, basic_packing, min_max);
134122
match p {
135123
Packing::_256 => out.extend_from_slice(bytes),
136124
Packing::_16 => pack_arithmetic::<16>(bytes, out),

‎src/pack_ints.rs‎

Lines changed: 7 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use crate::consume::{consume_byte, consume_byte_arrays};
33
use crate::error::error;
44
use crate::fast::CowSlice;
55
use crate::pack::{invalid_packing, pack_bytes, unpack_bytes};
6+
use crate::pack_shared::PackingTrait;
67
use crate::Error;
78
use alloc::vec::Vec;
89
use bytemuck::Pod;
@@ -19,7 +20,7 @@ enum Packing {
1920
_8,
2021
}
2122

22-
impl Packing {
23+
impl PackingTrait for Packing {
2324
fn new<T: SizedUInt>(max: T) -> Self {
2425
let max: u128 = max.try_into().unwrap_or_else(|_| unreachable!()); // From<usize> isn't implemented for u128.
2526
#[allow(clippy::match_overlapping_arm)] // Just make sure not to reorder them.
@@ -37,7 +38,9 @@ impl Packing {
3738
// Also makes no packing with offset_by_min = true is unrepresentable.
3839
out.push((self as u8 - Self::new(T::MAX) as u8) * 2 - offset_by_min as u8);
3940
}
41+
}
4042

43+
impl Packing {
4144
fn read<T: SizedUInt>(input: &mut &[u8]) -> Result<(Self, bool)> {
4245
let v = consume_byte(input)?;
4346
let p_u8 = crate::nightly::div_ceil_u8(v, 2) + Self::new(T::MAX) as u8;
@@ -329,18 +332,8 @@ impl SizedUInt for u8 {
329332
}
330333
}
331334

332-
pub fn minmax<T: SizedInt>(v: &[T]) -> (T, T) {
333-
let mut min = T::MAX;
334-
let mut max = T::MIN;
335-
for &v in v.iter() {
336-
min = min.min(v);
337-
max = max.max(v);
338-
}
339-
(min, max)
340-
}
341-
342335
fn skip_packing<T: SizedInt>(length: usize) -> bool {
343-
// Be careful using size_of::<T> since usize can be 4 or 8.
336+
// T is SizedInt, so it cannot be usize/isize, therefore comparisions against size are safe.
344337
if core::mem::size_of::<T>() == 1 {
345338
return true; // u8s can't be packed by pack_ints (only pack_bytes).
346339
}
@@ -370,31 +363,9 @@ fn pack_ints_sized<T: SizedInt>(ints: &mut [T], out: &mut Vec<u8>) {
370363
let (basic_packing, min_max) = if skip_packing::<T>(ints.len()) {
371364
(Packing::new(T::Unsigned::MAX), None)
372365
} else {
373-
// Take a small sample to avoid wastefully scanning the whole slice.
374-
let (sample, remaining) = ints.split_at(ints.len().min(16));
375-
let (min, max) = minmax(sample);
376-
377-
// Only have to check packing(max - min) since it's always as good as packing(max).
378-
let none = Packing::new(T::Unsigned::MAX);
379-
if Packing::new(max.to_unsigned().wrapping_sub(min.to_unsigned())) == none {
380-
none.write::<T::Unsigned>(out, false);
381-
(none, None)
382-
} else {
383-
let (remaining_min, remaining_max) = minmax(remaining);
384-
let min = min.min(remaining_min);
385-
let max = max.max(remaining_max);
386-
387-
// Signed ints pack as unsigned ints if positive.
388-
let basic_packing = if min >= T::default() {
389-
Packing::new(max.to_unsigned())
390-
} else {
391-
none // Any negative can't be packed without offset_packing.
392-
};
393-
(basic_packing, Some((min, max)))
394-
}
366+
crate::pack_shared::basic_packing_and_signed_min_max_cast_to_unsigned(ints, out)
395367
};
396368
let ints = bytemuck::must_cast_slice_mut(ints);
397-
let min_max = min_max.map(|(min, max)| (min.to_unsigned(), max.to_unsigned()));
398369
pack_ints_sized_unsigned::<T::Unsigned>(ints, out, basic_packing, min_max);
399370
}
400371

@@ -405,24 +376,7 @@ fn pack_ints_sized_unsigned<T: SizedUInt>(
405376
basic_packing: Packing,
406377
min_max: Option<(T, T)>,
407378
) {
408-
let p = if let Some((min, max)) = min_max {
409-
// If subtracting min from all ints results in a better packing do it, otherwise don't bother.
410-
let offset_packing = Packing::new(max.wrapping_sub(min));
411-
if offset_packing > basic_packing && ints.len() > 5 {
412-
for b in ints.iter_mut() {
413-
*b = b.wrapping_sub(min);
414-
}
415-
offset_packing.write::<T>(out, true);
416-
T::write(min, out);
417-
offset_packing
418-
} else {
419-
basic_packing.write::<T>(out, false);
420-
basic_packing
421-
}
422-
} else {
423-
basic_packing
424-
};
425-
379+
let p = crate::pack_shared::offset_packing(ints, out, basic_packing, min_max);
426380
match p {
427381
Packing::_128 => T::pack128(ints, out),
428382
Packing::_64 => T::pack64(ints, out),

‎src/pack_shared.rs‎

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
use crate::pack_ints::{SizedInt, SizedUInt};
2+
3+
pub trait PackingTrait: Copy + PartialOrd {
4+
fn new<T: SizedUInt>(max: T) -> Self;
5+
6+
fn write<T: SizedUInt>(self, out: &mut Vec<u8>, offset_by_min: bool);
7+
}
8+
9+
fn minmax<T: SizedInt>(v: &[T]) -> (T, T) {
10+
let mut min = T::MAX;
11+
let mut max = T::MIN;
12+
for &v in v.iter() {
13+
min = min.min(v);
14+
max = max.max(v);
15+
}
16+
(min, max)
17+
}
18+
19+
// Writes a packing to `out` iff it returns None.
20+
pub fn basic_packing_and_signed_min_max_cast_to_unsigned<T: SizedInt, P: PackingTrait>(
21+
ints: &[T],
22+
out: &mut Vec<u8>,
23+
) -> (P, Option<(T::Unsigned, T::Unsigned)>) {
24+
// Take a small sample to avoid wastefully scanning the whole slice.
25+
// Note: This small sample is purely an optimization, it has no impact on the encoded result
26+
// because we only use it to bail from scanning the entire slice if the first 16-32 elements
27+
// cannot be packed.
28+
let sample_size = (32 / std::mem::size_of::<T>()).max(16);
29+
let (sample, remaining) = ints.split_at(ints.len().min(sample_size));
30+
let (min, max) = minmax(sample);
31+
32+
// Only have to check packing(max - min) since it's always as good as packing(max).
33+
let none = P::new(T::Unsigned::MAX);
34+
if P::new(max.to_unsigned().wrapping_sub(min.to_unsigned())) == none {
35+
none.write::<T::Unsigned>(out, false);
36+
(none, None)
37+
} else {
38+
let (remaining_min, remaining_max) = minmax(remaining);
39+
let min = min.min(remaining_min);
40+
let max = max.max(remaining_max);
41+
42+
// Signed ints pack as unsigned ints if positive.
43+
let basic_packing = if min >= T::default() {
44+
P::new(max.to_unsigned())
45+
} else {
46+
none // Any negative can't be packed without offset_packing.
47+
};
48+
49+
(basic_packing, Some((min.to_unsigned(), max.to_unsigned())))
50+
}
51+
}
52+
53+
// Writes a packing to `out` iff `min_max` is Some.
54+
pub fn offset_packing<T: SizedUInt, P: PackingTrait>(
55+
ints: &mut [T],
56+
out: &mut Vec<u8>,
57+
basic_packing: P,
58+
min_max: Option<(T, T)>,
59+
) -> P {
60+
if let Some((min, max)) = min_max {
61+
// If subtracting min from all ints results in a better packing do it, otherwise don't bother.
62+
let offset_packing = P::new(max.wrapping_sub(min));
63+
// TODO(breaking change) don't hardcode this as 5. Only perform offset_packing
64+
// on a few elements if the added T::write(min, out) makes it still smaller.
65+
let small_skip_offset_packing = 5;
66+
if offset_packing > basic_packing && ints.len() > small_skip_offset_packing {
67+
for b in ints.iter_mut() {
68+
*b = b.wrapping_sub(min);
69+
}
70+
offset_packing.write::<T>(out, true);
71+
T::write(min, out);
72+
offset_packing
73+
} else {
74+
basic_packing.write::<T>(out, false);
75+
basic_packing
76+
}
77+
} else {
78+
basic_packing
79+
}
80+
}

0 commit comments

Comments
 (0)