Skip to content

Avoid quadratic buffer copies in EnvelopeReader streaming path#304

Merged
anuraaga merged 2 commits into
connectrpc:mainfrom
dongjiang1989:optimization-buffer
Jul 15, 2026
Merged

Avoid quadratic buffer copies in EnvelopeReader streaming path#304
anuraaga merged 2 commits into
connectrpc:mainfrom
dongjiang1989:optimization-buffer

Conversation

@dongjiang1989

Copy link
Copy Markdown
Contributor

Summary

  • Use in-place del buffer[:n] instead of buffer = buffer[n:] in EnvelopeReader._read_messages()
  • Avoid allocating a new bytearray and copying all remaining bytes on every message
  • For streaming RPCs with N messages, reduces total bytes copied from O(N²) to O(N)

Background

bytearray[n:] creates a new bytearray and copies all remaining bytes. In the envelope reader's message loop, this happened after every message was consumed:

# Before
self._buffer = self._buffer[5 + self._next_message_length :]  # allocates + copies

In a long-running streaming RPC receiving N messages, the cumulative copy volume becomes:

  • After message 1: copy (N-1) messages of remaining data
  • After message 2: copy (N-2) messages of remaining data
  • ...
  • Total: O(N²) bytes copied

The fix uses in-place deletion, which reuses the existing buffer via C-level memmove:

# After
del self._buffer[: 5 + self._next_message_length]  # in-place memmove, no allocation

Benchmark Results

A benchmark was run comparing the old and new implementation across several streaming scenarios. Each scenario was executed 50 times and averaged.

Scenario Before After Speedup
Single feed: 1000 msgs × 1KB 23.482 ms (41.8 MB/s) 0.352 ms (2784 MB/s) 66.7x
Single feed: 100 msgs × 10KB 2.256 ms (433 MB/s) 0.086 ms (11420 MB/s) 26.2x
Single feed: 100 msgs × 1KB 0.162 ms (604 MB/s) 0.043 ms (2293 MB/s) 3.8x
Streaming: 1000 msgs × 1KB, 64B chunks 2.961 ms (331 MB/s) 2.934 ms (335 MB/s) 1.0x
Streaming: 500 msgs × 10KB, 256B chunks 3.868 ms (1263 MB/s) 4.032 ms (1212 MB/s) 0.96x
Streaming: 100 msgs × 10KB, 256B chunks 0.778 ms (1255 MB/s) 0.828 ms (1180 MB/s) 0.94x
Streaming: 100 msgs × 1KB, 64B chunks 0.410 ms (239 MB/s) 0.283 ms (347 MB/s) 1.4x

Quadratic Behavior Confirmation

The "Single feed" scenarios clearly expose the O(n²) behavior in the original implementation:

Message Count Before Theoretical O(n) Actual Ratio
100 msgs × 1KB 0.162 ms baseline baseline
1000 msgs × 1KB 23.482 ms 1.62 ms (10x) 23.5 ms (145x!)

A 10x increase in data resulted in a 145x slowdown in the original code, closely matching the theoretical 10² = 100x expected from quadratic scaling.

Streaming Scenarios

Chunked feeding (64B/256B chunks) does not accumulate large buffers, so the quadratic effect is less pronounced. However, the fix still eliminates the latent risk of quadratic behavior for large-buffer cases and provides modest gains for small buffers.

Conclusion

The fix eliminates quadratic memory copying in the streaming RPC hot path, delivering 3.8x to 66.7x throughput improvement depending on workload. Memory allocations drop from O(N) to O(1) per message, and total bytes copied drops from O(N²) to O(N). This is particularly important for:

  • Long-running streaming RPCs with many messages
  • Scenarios where a large chunk of data is fed to the reader at once
  • Memory-constrained environments where allocation pressure matters

Use in-place `del buffer[:n]` instead of `buffer = buffer[n:]` to avoid
allocating a new bytearray and copying all remaining bytes on every
message. For long-running streaming RPCs with N messages, this reduces
total bytes copied from O(N²) to O(N), yielding up to 66x throughput
improvement in single-feed scenarios.

Signed-off-by: dongjiang <dongjiang1989@126.com>
@dongjiang1989 dongjiang1989 changed the title perf: avoid O(n²) buffer copies in EnvelopeReader streaming path Avoid O(n²) buffer copies in EnvelopeReader streaming path Jul 15, 2026
@dongjiang1989

Copy link
Copy Markdown
Contributor Author

cc @anuraaga PTAL

Remove comments explaining in-place deletion for buffer.

Signed-off-by: Anuraag (Rag) Agrawal <anuraaga@gmail.com>
@anuraaga anuraaga changed the title Avoid O(n²) buffer copies in EnvelopeReader streaming path Avoid quadratic buffer copies in EnvelopeReader streaming path Jul 15, 2026
@anuraaga

Copy link
Copy Markdown
Collaborator

Thanks @dongjiang1989 - while the large single feed case should be rare in practice, the tweak is simple enough to go with. Note that I think the reason it improves isn't because of memmove, which would only reduce allocations but not copies, but because bytearray itself amortizes prefix deletion by keeping track of a start ptr and reclaiming with heuristics. I think these heuristics will work fine for the normal streaming case so no worries.

@anuraaga anuraaga merged commit d91cdfd into connectrpc:main Jul 15, 2026
24 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants