drpcstream: introduce shared BufferPool for ring buffer#55
Conversation
|
I had an idea which I ran through claude and below is the summary of it. I'm not planning to do it but in future we can re-consider if profiling shows any gain. Buffer pool: further optimization ideas Right now the data copy chain for an incoming message looks like this:
We could eliminate copy #2 by having the packet assembler get its buffer from the pool directly. The assembler already has a TODO for buffer reuse. Instead of reusing its own backing array across packets (lines 84-87), it would Another idea: size-bucketed pools (e.g. 1KiB, 16KiB, 32KiB) so that I think we should keep the pool simple for now. The assembler integration is the more interesting optimization since it removes a full copy per message. Worth revisiting once we have benchmarks to measure the actual impact. |
a17330d to
b91bf1b
Compare
cafa1dc to
b3d2355
Compare
b91bf1b to
a58986c
Compare
b3d2355 to
38c84dc
Compare
38c84dc to
f2f767f
Compare
Add a BufferPool backed by sync.Pool that is shared across all streams within a Manager. The ring buffer now obtains buffers from the pool on Enqueue and transfers ownership to the caller on Dequeue, which advances the tail immediately. This removes the two-step Dequeue/Done protocol and simplifies Close (no longer needs to wait for held buffers). The pool is a required parameter in the Stream constructor, created once per Manager and passed to all streams it creates. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
f2f767f to
a305254
Compare
| // stream receive path. Buffers obtained via Get should be returned via | ||
| // Put when no longer needed. Forgetting to Put is safe (GC reclaims) | ||
| // but reduces reuse. | ||
| type BufferPool struct { |
| } | ||
|
|
||
| rb.buf[rb.head] = append(rb.buf[rb.head][:0], data...) | ||
| b := rb.pool.Get() |
There was a problem hiding this comment.
These can me move outside the lock now.
| // TODO(shubham): remove this method once a shared buffer pool is introduced. | ||
| // With a pool, Dequeue will advance the tail immediately and the caller will | ||
| // return the buffer to the pool directly. | ||
| func (rb *ringBuffer) Done() { |
There was a problem hiding this comment.
Given the buffer pool changes, we can retain the contract of ringBuffer and keep Done() to release the buffer back to the pool. That way, the consumer doesn't have to know about the internals of ring buffer whether it is using the pool or fixed buffers.
| data = append([]byte(nil), data...) | ||
| s.recvQueue.Done() | ||
| data = append([]byte(nil), *b...) | ||
| s.pool.Put(b) |
There was a problem hiding this comment.
We should continue to use s.recvQueue.Done(). Refer to other comments for context.
Add a BufferPool backed by sync.Pool that is shared across all streams
within a Manager. The ring buffer now obtains buffers from the pool on
Enqueue and transfers ownership to the caller on Dequeue, which advances
the tail immediately. This removes the two-step Dequeue/Done protocol
and simplifies Close (no longer needs to wait for held buffers).
The pool is a required parameter in the Stream constructor, created once
per Manager and passed to all streams it creates.