Skip to content

codegen: recursion-aware inline storage for singular message fields (PointerRepr::Inline + unbox_message_fields) #248

Description

@iainmcgin

Summary

Add a PointerRepr::Inline variant (built-in ProtoBox<T> impl that stores T directly, no heap) and an unbox_message_fields() / unbox_message_fields_in() config knob that mirrors unbox_oneof(): it inlines every non-recursive singular message field by default, with the same recursion detection that keeps recursive fields on Box.

Motivation

The rust_serialization_benchmark integration shows buffa's owned-decode and populate paths are 2–3× slower than prost on the mesh and mk48 datasets, while encode is actually faster than prost on three of four datasets. The gap is entirely explained by MessageField<T> defaulting to Option<Box<T>>: mesh is 125k Triangles × 4 Vector3 submessages = ~500k heap allocations that prost avoids by emitting Option<Vector3> inline (it boxes only on detected recursion).

dataset buffa 0.7.1 deserialize prost 0.14.1 Δ
mesh 26.2 ms 13.2 ms +98%
mk48 15.8 ms 8.73 ms +81%

(Numbers from the post-merge CI run.)

We already have:

So the recursion-detection machinery exists, the pointer-parameter plumbing exists, but there is no built-in inline pointer and no recursion-aware "inline everything safe" knob for singular fields.

Proposed change

  1. buffa::Inline<T> (or similar) — a #[repr(transparent)] newtype around T implementing ProtoBox<T>. Shipped in buffa so users don't need a crate-local newtype for the common case.
  2. PointerRepr::Inline — selects it; codegen emits MessageField<T, ::buffa::Inline<T>>.
  3. Config::unbox_message_fields() / unbox_message_fields_in(&[...]) — applies PointerRepr::Inline to every singular message field under the prefix, reusing the unboxing_is_recursive analysis so recursive fields silently stay on Box (and an exact-path rule that names a recursive field errors, matching unbox_oneof_in).

Default stays Box for now (no codegen drift). Whether to flip the default to recursion-aware-inline in a future release is a separate decision — prost made that trade-off and it's the reason it wins this benchmark, but it does inflate parent struct size when a large submessage is rarely present.

Validation

Re-run rust_serialization_benchmark with unbox_message_fields() enabled in the buffa build.rs; mesh/mk48 deserialize and populate should close to within ±10% of prost (encode is already ahead).

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions