Summary
The receive path allocates a new Vec<u8> on every message across all transports, violating the project's own measurement accuracy guideline in AGENTS.md:
Avoid allocations in send/receive hot loops.
This adds allocator jitter to every measured latency sample. Sporadic allocator overhead (bin traversal, mmap/brk for new pages) inflates tail percentiles with noise from the tool, not from the transport.
Affected Code Paths
TCP/UDS (blocking): bincode::deserialize() constructs a new Message with a new Vec<u8> payload on every receive.
SHM ring buffer (blocking): read_data_blocking() calls Vec::with_capacity(data_len) per message (avoids zero-fill but still allocates).
SHM-direct: receive_blocking() calls Vec::with_capacity(payload_len) per message.
PMQ (blocking): vec![0u8; self.max_msg_size] per receive — allocates AND zero-fills.
Expected Behavior
Pre-allocate the receive buffer once at setup and reuse it across all iterations, similar to what standalone_client.rs already does for the send side ("Create message once, reuse across iterations to avoid per-message heap allocation").
For transports where deserialization produces a new Message struct (bincode), the deserialization target should be a pre-allocated buffer that is overwritten in-place rather than a fresh allocation.
Impact
Estimated overhead per allocation: ~50-200ns on the fast path (glibc ptmalloc2 same-size reuse), with occasional spikes of 1-10µs when the allocator coalesces or calls mmap. On a benchmark measuring 25-100µs RTT, this is 0.2-4% of the signal at median, and a significant contributor to tail noise.
Context
Identified during a comparative evaluation of IPC benchmark approaches. A comparable C++20 benchmark allocates one packet buffer at startup and receives directly into it — zero per-message allocations in the measured path. The difference is visible in tail latency stability.
Summary
The receive path allocates a new
Vec<u8>on every message across all transports, violating the project's own measurement accuracy guideline in AGENTS.md:This adds allocator jitter to every measured latency sample. Sporadic allocator overhead (bin traversal,
mmap/brkfor new pages) inflates tail percentiles with noise from the tool, not from the transport.Affected Code Paths
TCP/UDS (blocking):
bincode::deserialize()constructs a newMessagewith a newVec<u8>payload on every receive.SHM ring buffer (blocking):
read_data_blocking()callsVec::with_capacity(data_len)per message (avoids zero-fill but still allocates).SHM-direct:
receive_blocking()callsVec::with_capacity(payload_len)per message.PMQ (blocking):
vec![0u8; self.max_msg_size]per receive — allocates AND zero-fills.Expected Behavior
Pre-allocate the receive buffer once at setup and reuse it across all iterations, similar to what
standalone_client.rsalready does for the send side ("Create message once, reuse across iterations to avoid per-message heap allocation").For transports where deserialization produces a new
Messagestruct (bincode), the deserialization target should be a pre-allocated buffer that is overwritten in-place rather than a fresh allocation.Impact
Estimated overhead per allocation: ~50-200ns on the fast path (glibc ptmalloc2 same-size reuse), with occasional spikes of 1-10µs when the allocator coalesces or calls
mmap. On a benchmark measuring 25-100µs RTT, this is 0.2-4% of the signal at median, and a significant contributor to tail noise.Context
Identified during a comparative evaluation of IPC benchmark approaches. A comparable C++20 benchmark allocates one packet buffer at startup and receives directly into it — zero per-message allocations in the measured path. The difference is visible in tail latency stability.