Skip to content

encode: write every BufMut through one pre-sized cursor - #467

Merged
iainmcgin merged 2 commits into
mainfrom
iain/single-sink-encode
Sep 26, 2026
Merged

iainmcgin merged 2 commits into
mainfrom
iain/single-sink-encode

Conversation

@iainmcgin

Copy link
Copy Markdown
Collaborator

A generated write_to is instantiated once per sink type, so a program that encodes into Vec<u8>, BytesMut, and a Rope carries three copies of every message's write code. This change writes every BufMut through one concrete cursor, so the BufMut sinks share one instance per message.

On the whatsapp waproto schema (334 messages, 3,477 fields; fat LTO, text + data), a program that encodes into three sinks shrinks by 6.2% at opt-level = "z", 12.7% at s, and 23.0% at 3. A program that encodes into one sink grows 0.8% at z and 0.5% at s, and shrinks 16.0% at 3. On bare metal (c7i.metal-24xl, ±5% run-to-run, LTO), encoding into a BytesMut takes 0.19–0.63 of the time it did and into a fresh Vec 0.57–0.73, encode_to_vec and encode_to_bytes are within 0.97–1.07 of it, and encoding into a reused Vec takes 0.99–1.07, up to 7% longer.

When the BufMut's current chunk has room for the whole message, EncodeSink::__write_pre_sized gives write_to a bounds-checked PreSized cursor over that spare capacity, then advances the buffer by the bytes written. A shorter chunk (a fresh Vec::new() offers 64 bytes) is encoded into an exact-size scratch Vec and appended with one put_slice. Rope and other non-BufMut sinks still receive every write individually, and lazy views and DynamicMessage encode as before.

Behaviour changes:

  • For a BufMut sink, a write_to that writes more bytes than compute_size reported panics in every build. Before, it panicked only with debug assertions or grew the buffer. One that writes fewer bytes panics with debug assertions, now from every encode entry point.
  • A BufMut wrapper that overrides put_slice no longer sees the message bytes through put_*; they arrive through advance_mut, or through one put_slice when the message is staged.

Four unsafe blocks. In __write_pre_sized, one views the chunk's first len bytes as [MaybeUninit<u8>] after checking chunk.len() >= len, and one calls advance_mut(written). PreSized::put_slice calls copy_nonoverlapping after a bounds check, and write_to_new_vec calls set_len(written). Every store is bounds-checked and written counts only bytes stored, so no byte past written is claimed.

The alternative to staging a short chunk is calling reserve on the buffer. BufMut has no reserve, so that would need a separate Vec-only path.

New hidden public items: EncodeSink::__PRE_SIZED, EncodeSink::__write_pre_sized, and PreSized. The #437 changelog fragment loses its note that encoding into a BytesMut is slow, which this change fixes.

The provided encode methods on Message and ViewEncode now compute the
exact size, then write any BufMut into that many contiguous bytes of its
spare capacity through a shared bounds-checked cursor (PreSized). Each
message's write_to is compiled once for all BufMut sinks instead of once
per sink type, and encoding into a BytesMut with room no longer makes a
call per tag and varint byte.

A sink with less room than the message is filled through a scratch Vec
and appended with one put_slice. Rope and types that implement
EncodeSink without BufMut still receive every write individually.
Generated lazy views and DynamicMessage are unchanged.

A write_to that produces more bytes than compute_size declared now
panics in release builds too, and in debug builds one that produces
fewer panics in every encode entry point.
@github-actions

Copy link
Copy Markdown

All contributors have signed the CLA ✍️ ✅
Posted by the CLA Assistant Lite bot.

@iainmcgin
iainmcgin added this pull request to the merge queue Sep 26, 2026
Merged via the queue into main with commit 851fa2f Sep 26, 2026
11 checks passed
@iainmcgin
iainmcgin deleted the iain/single-sink-encode branch September 26, 2026 19:59
@github-actions github-actions Bot locked and limited conversation to collaborators Sep 26, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants