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
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.
PointerRepr::Inline — selects it; codegen emits MessageField<T, ::buffa::Inline<T>>.
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).
Summary
Add a
PointerRepr::Inlinevariant (built-inProtoBox<T>impl that storesTdirectly, no heap) and anunbox_message_fields()/unbox_message_fields_in()config knob that mirrorsunbox_oneof(): it inlines every non-recursive singular message field by default, with the same recursion detection that keeps recursive fields onBox.Motivation
The rust_serialization_benchmark integration shows buffa's owned-decode and populate paths are 2–3× slower than prost on the
meshandmk48datasets, while encode is actually faster than prost on three of four datasets. The gap is entirely explained byMessageField<T>defaulting toOption<Box<T>>:meshis 125kTriangles × 4Vector3submessages = ~500k heap allocations that prost avoids by emittingOption<Vector3>inline (it boxes only on detected recursion).(Numbers from the post-merge CI run.)
We already have:
unbox_oneof()— recursion-aware inlining for oneof variants (buffa-codegen: configurable Box wrapping for message-type oneof variants #126).box_type/box_type_custom(Pluggable owned pointer for message fields (ProtoBox) #209) — pluggable pointer for singular fields, butPointerRepris{Box, Custom}only, the sole built-inProtoBoximpl isBox<T>, and the docs explicitly steer users away from a blanket inline.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
buffa::Inline<T>(or similar) — a#[repr(transparent)]newtype aroundTimplementingProtoBox<T>. Shipped inbuffaso users don't need a crate-local newtype for the common case.PointerRepr::Inline— selects it; codegen emitsMessageField<T, ::buffa::Inline<T>>.Config::unbox_message_fields()/unbox_message_fields_in(&[...])— appliesPointerRepr::Inlineto every singular message field under the prefix, reusing theunboxing_is_recursiveanalysis so recursive fields silently stay onBox(and an exact-path rule that names a recursive field errors, matchingunbox_oneof_in).Default stays
Boxfor 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 buffabuild.rs; mesh/mk48 deserialize and populate should close to within ±10% of prost (encode is already ahead).