[F52.3] mxaccess-codec: caller-supplied scratch buffer for write encoder
rust / build / test / clippy / fmt (push) Has been cancelled
rust / cargo public-api drift check (F41) (push) Has been cancelled

Adds `write_message::encode_into_bytes_mut` (and the timestamped
variant) which writes the encoded body into a caller-supplied
`BytesMut`. The buffer is cleared and resized in place each call;
once it has grown to the largest body the session will produce, it
allocates nothing further.

A session that holds a single `BytesMut` and reuses it across writes:

  - Int32 / Float32 / Float64: 2 → 1 allocs/op
    (only the `encode_scalar_value` scratch `Vec<u8>` remains)
  - Boolean: 1 → 0 allocs/op
    (no per-value scratch — the literal payload is a stack `[u8; 4]`)

Bench delta in `design/M6-bench-baseline.md` § F52.3. The
`encode_scalar_value` Vec is the remaining 1 alloc/op for fixed-width
scalars; eliminating it would require inlining the LE-bytes write
into the body slice (left for a follow-up since the F52 spec only
asks for 2 → 1).

Resolves F52 (all three optimisations landed: 4e76b44 F52.1,
a0fa5be F52.2, this commit F52.3). Existing `encode` / `encode_to_bytes_mut`
public surface unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Joseph Doherty
2026-05-06 22:53:07 -04:00
parent a0fa5bedfd
commit ceeaeefa71
4 changed files with 114 additions and 5 deletions
+27
View File
@@ -23,6 +23,8 @@ The bench gates on this: any `write_message::encode` scenario at
| `write_message::encode` (Boolean) | 10,000 | 1.00 | 37 | 1.00 |
| `write_message::encode` (String, 5 chars) | 10,000 | 4.00 | 92 | 4.00 |
| `write_message::encode_to_bytes_mut` (Int32) | 10,000 | 2.00 | 44 | 2.00 |
| `encode_into_bytes_mut` (Int32, pooled, F52.3) | 10,000 | 1.00 | 4 | 1.00 |
| `encode_into_bytes_mut` (Bool, pooled, F52.3) | 10,000 | 0.00 | 0 | 0.00 |
| `MxReferenceHandle::from_names` (F52.2) | 10,000 | 0.00 | 0 | 0.00 |
| `NmxSubscriptionMessage::parse_inner` | 10,000 | 1.00 | 72 | 1.00 |
| (DataUpdate, Int32) | | | | |
@@ -99,6 +101,31 @@ Cold-path (first call with a new name) still pays the
on repeats. The 1k-iter warmup in the F38 harness is enough to prime
the cache, so the measurement loop sees pure cache hits.
### F52.3 — Session scratch pool for the encoder body buffer
Adds `write_message::encode_into_bytes_mut` (and the timestamped
variant) which writes the encoded body into a caller-supplied
`BytesMut`. The buffer is cleared and resized in place each call;
once it has grown to the largest body the session will produce, it
allocates nothing further.
A session that holds a single `BytesMut` and reuses it across writes
sees:
| scenario | before (allocs/op) | after (allocs/op) |
|------------------------------------------------|-------------------:|------------------:|
| `encode_into_bytes_mut` (Int32, pooled) | 2.00 | 1.00 |
| `encode_into_bytes_mut` (Boolean, pooled) | 1.00 | 0.00 |
The remaining `1.00` for Int32 is the `encode_scalar_value` scratch
`Vec<u8>`. Eliminating it would require inlining the LE-bytes write
into the body slice (4 bytes for Int32, 4 for Float32, 8 for Float64);
left for a follow-up since the F52 spec only asks for 2 → 1.
Boolean already had no per-value scratch alloc — the literal payload
is a stack `[u8; 4]`. Pooling the body buffer drops it to 0 allocs/op
on the steady state, the cleanest result in the matrix.
## Reproducing
```powershell