diff --git a/Cargo.toml b/Cargo.toml index 86c98cf1e..1c84ad8db 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -50,6 +50,11 @@ name = "deku" harness = false required-features = ["alloc"] +[[bench]] +name = "bebits" +harness = false +required-features = ["alloc", "bits"] + [lints] workspace = true diff --git a/benches/bebits.rs b/benches/bebits.rs new file mode 100644 index 000000000..86ed0bcbe --- /dev/null +++ b/benches/bebits.rs @@ -0,0 +1,75 @@ +//! Big-endian bit-packed headers: the shape used by real network / space +//! protocols (CCSDS, IPv4, DVB-S2). Mirrors the CCSDS TM Transfer Frame +//! primary header, 6 octets / 11 fields. +use criterion::{criterion_group, criterion_main, Criterion}; +use deku::prelude::*; +use no_std_io::io::Cursor; + +#[derive(Debug, PartialEq, DekuRead, DekuWrite)] +#[deku(endian = "big")] +struct TmPrimaryHeader { + #[deku(bits = 2)] + tfvn: u8, + #[deku(bits = 10)] + scid: u16, + #[deku(bits = 3)] + vcid: u8, + #[deku(bits = 1)] + ocf: u8, + mcfc: u8, + vcfc: u8, + #[deku(bits = 1)] + tfs: u8, + #[deku(bits = 1)] + syn: u8, + #[deku(bits = 1)] + po: u8, + #[deku(bits = 2)] + sli: u8, + #[deku(bits = 11)] + fhp: u16, +} + +/// Same 6 octets, byte-aligned: the deku fast path, for scale. +#[derive(Debug, PartialEq, DekuRead, DekuWrite)] +#[deku(endian = "big")] +struct SixBytes { + a: u8, + b: u8, + c: u8, + d: u8, + e: u8, + f: u8, +} + +/// Worst case in the docs: a 1-bit field in a wide container. +#[derive(Debug, PartialEq, DekuRead, DekuWrite)] +#[deku(endian = "big")] +struct OneBitU64 { + #[deku(bits = 1)] + a: u64, +} + +fn bench(c: &mut Criterion) { + let buf = [0x2Au8, 0xB5, 0x11, 0x22, 0xC7, 0xFF, 0x00, 0x99]; + c.bench_function("be_tm_primary_header_11_fields", |b| { + b.iter(|| { + let mut r = Reader::new(Cursor::new(&buf)); + TmPrimaryHeader::from_reader_with_ctx(&mut r, ()).unwrap() + }) + }); + c.bench_function("be_six_bytes_aligned", |b| { + b.iter(|| { + let mut r = Reader::new(Cursor::new(&buf)); + SixBytes::from_reader_with_ctx(&mut r, ()).unwrap() + }) + }); + c.bench_function("be_one_bit_in_u64", |b| { + b.iter(|| { + let mut r = Reader::new(Cursor::new(&buf)); + OneBitU64::from_reader_with_ctx(&mut r, ()).unwrap() + }) + }); +} +criterion_group!(bebits, bench); +criterion_main!(bebits); diff --git a/src/impls/primitive.rs b/src/impls/primitive.rs index d35f8a2a0..edfee1dce 100644 --- a/src/impls/primitive.rs +++ b/src/impls/primitive.rs @@ -181,6 +181,22 @@ macro_rules! ImplDekuReadBits { } } + // Fast path: big-endian, Msb0 bit order. + // + // The generic paths below zero-extend the value to the container + // width by calling `BoundedBitVec::insert(0, false)` once per + // padding bit, and each `insert` shifts the entire backing array + // right by one bit. That is O(MAX_TYPE_BITS) bit operations per + // padding bit, i.e. O(MAX_TYPE_BITS * (MAX_TYPE_BITS - bit_size)) + // per field -- for a 1-bit field in a u64 that is 63 whole-array + // shifts. `load_be` performs the identical zero-extension in one + // step. Only Msb0 + big-endian is handled here; the Lsb0 and + // little-endian paths keep their existing behaviour. + if !input_is_le && order == Order::Msb0 && bit_size > 0 { + let value = bit_slice.load_be::<$inner>(); + return Ok((bit_size, <$typ>::from_be_bytes(value.to_be_bytes()))); + } + // if read from Lsb order and it's especially cursed since its not just within one byte... // read_bits returned: [0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 1] // | second | first | @@ -286,6 +302,15 @@ macro_rules! ImplDekuReadBits { } } + // Fast path: big-endian. This impl has no `Order` parameter and + // so is always Msb0. See the comment on the same fast path in + // the `(Endian, BitSize, Order)` impl above for why the generic + // path below is quadratic in the container width. + if !input_is_le && bit_size > 0 { + let value = bit_slice.load_be::<$inner>(); + return Ok((bit_size, <$typ>::from_be_bytes(value.to_be_bytes()))); + } + // Create a new BoundedBitVec from the slice and pad un-aligned chunks // i.e. [10010110, 1110] -> [10010110, 00001110] const MAX_TYPE_BYTES: usize = core::mem::size_of::<$typ>();