Skip to content

let DekuBitField fields join a bit-field run with bit_field - #694

Open
Simon-Calbert-Aerospacelab wants to merge 2 commits into
sharksforarms:masterfrom
Simon-Calbert-Aerospacelab:perf/batch-enum-bit-fields
Open

Simon-Calbert-Aerospacelab wants to merge 2 commits into
sharksforarms:masterfrom
Simon-Calbert-Aerospacelab:perf/batch-enum-bit-fields

Conversation

@Simon-Calbert-Aerospacelab

Copy link
Copy Markdown
Contributor

The problem

#677 made deku read a group of neighbouring bit fields with a single call instead of one call per field. That only works for plain integers and bool.

Real headers are mostly flags, and a protocol crate usually models a flag as an enum rather than a u8. When the derive meets a field typed OcfFlag, it only sees a type name and can't know how many bits it takes, so the group stops there. In a CCSDS TM primary header (11 fields, 5 of them enums), that means 8 calls instead of 1.

The fix

Two small pieces let an enum tell the derive its width:

  1. A new trait, DekuBitField, that gives a type's width in bits and converts it from and to raw bits. You can derive it:

    #[derive(DekuRead, DekuWrite, DekuBitField)]
    #[deku(id_type = "u8", bits = 1, endian = "endian", ctx = "endian: deku::ctx::Endian")]
    enum OcfFlag {
        #[deku(id = 0)]
        Absent,
        #[deku(id = 1)]
        Present,
    }
  2. A field attribute, #[deku(bit_field)], that lets such a field join the group:

    #[derive(DekuRead, DekuWrite)]
    #[deku(endian = "big")]
    struct TmPrimaryHeader {
        #[deku(bits = 2)]
        version: u8,
        #[deku(bits = 10)]
        scid: u16,
        #[deku(bits = 3)]
        vcid: u8,
        #[deku(bit_field)]
        ocf: OcfFlag,
        // ...
    }

With this, the whole 48-bit header becomes one read and one write.

Why an attribute on the field

The derive can't check whether a type implements a trait, so the field has to say so. If a marked type doesn't implement DekuBitField, you get an ordinary "trait bound not satisfied" error pointing at that field.

What happens when the width isn't known in advance

The derive groups fields before it knows the enum widths. So the generated code makes the final choice at compile time:

  • If the group fits in 64 bits, it is read and written in one call.
  • If it turns out wider, it quietly falls back to one call per field. This never causes a compile error.
  • Following Add Ci grungraun; Fix 6 simple byte read regression; Add Protocol Benchmarks #682, a group made only of whole bytes (for example an 8-bit enum between u8 fields) also keeps one call per field, because that is faster for plain bytes.

Safety

A batched read accepts and rejects exactly the same ids as a normal read. The "no matching variant" error comes from the same code in both cases. The one difference is on input too short for the whole group: the batched read reports Incomplete before checking any id, where the normal read may report the bad id first.

A batched write rejects the same values a normal write does: for a derived enum, an id too wide for bits is a compile error, and a hand-written to_bit_run returns the error itself. In debug builds, a to_bit_run that returns bits above its width is caught by an assertion.

Results

bebits, 128 headers per iteration, same struct with and without the attribute:

CCSDS TM header, per header Without With Speed-up
Read 11.3 ns 8.0 ns about 1.4x
Write 109 ns 12.2 ns about 9x

The write gain is the larger one because, without batching, each field is spliced into the output bit by bit. With batching, the fields are combined into one integer and written once.

Structs that don't use bit_field are not affected: their generated code is identical to master.

Reproduce with cargo bench --bench bebits --all-features.

Tests

  • Unit tests for how fields are grouped, next to the existing ones from perf: read and write a run of adjacent bit fields in one call #677.
  • A randomized test comparing marked and unmarked versions of the same struct on 20,000 inputs, plus targeted round-trip and error tests.
  • Compile-fail tests for misuse of the attribute and the derive, with MSRV and beta snapshots.
  • The full CI feature matrix, clippy with -D warnings, all examples, and the no_std builds pass locally.

What DekuBitField can be used on

With #[derive(DekuBitField)]: simple enums only. The derive accepts an enum whose value is just its id on the wire:

  • non-generic, and every variant is a unit variant (no data inside);
  • each variant has an integer id (id = 1 or id = "0x01") that fits in bits;
  • id_type is u8, u16, u32 or u64, and bits, if set, is an integer literal from 1 up to that width;
  • bit order is Msb0 (the default), and an id wider than 8 bits must be big-endian or take its endian from context.

Anything that needs more than the id is refused with a compile error naming the rule: id_pat, default, a variant reader or writer, magic, bytes, seek_*, or an id taken from context.

By hand: any type. A struct, a newtype, or a more complex enum can implement the trait itself:

struct Nibble(u8);

impl DekuBitField for Nibble {
    const BITS: usize = 4;
    fn from_bit_run(raw: u64) -> Result<Self, DekuError> {
        Ok(Self((raw & 0xF) as u8))
    }
    fn to_bit_run(&self) -> Result<u64, DekuError> {
        deku::writer::check_bit_size::<false>(u64::from(self.0), 4)?;
        Ok(u64::from(self.0))
    }
}

The type must also keep its normal DekuRead/DekuWrite with an Endian context. When a group can't be batched, the generated code falls back to that normal read or write for each field.

Where it applies. A bit_field is only batched when the struct holding it is explicitly big-endian and uses Msb0. In any other struct, the field is read and written on its own, as before.

wcampbell0x2a
wcampbell0x2a previously approved these changes Nov 1, 2026
@Simon-Calbert-Aerospacelab

Copy link
Copy Markdown
Contributor Author

@wcampbell0x2a Thanks for the approval, I just did a rebased to fix the small conflicts.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants