Skip to content

Commit ebdefa2

Browse files
committed
perf(chunkreader): avoid buffer pool thrashing on reset
The reset path unconditionally swaps oversized buffers back to minBufSize, causing repeated pool Get/Put cycles when workloads alternate between small and large reads. Add 4x threshold before downsizing - keeps modestly oversized buffers, only reclaims when truly excessive. Also cache *r.buf to local variable to reduce pointer dereferences in hot path. LargeSmallAlternating (16KB/100B cycles, 500 iterations): Original: 450µs 1.05MB/op 131 allocs/op Optimized: 47µs 16KB/op 4 allocs/op Result: 9.5x faster, 98% less allocation RandomSizes (1-16KB, 1000 iterations): Original: 13.6ms 6.5MB/op 1015 allocs/op Optimized: 11.5ms 98KB/op 13 allocs/op Result: 1.2x faster, 98% less allocation PGMessagePattern (5B header + 1KB body, 1000 iterations): Original: 30µs 8KB/op 4 allocs/op Optimized: 28µs 8KB/op 4 allocs/op Result: ~1x (no thrashing in baseline) Signed-off-by: Mathias Bogaert <mathias.bogaert@gmail.com>
1 parent e214c23 commit ebdefa2

1 file changed

Lines changed: 21 additions & 12 deletions

File tree

pgproto3/chunkreader.go

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -43,48 +43,57 @@ func newChunkReader(r io.Reader, minBufSize int) *chunkReader {
4343
// Next returns buf filled with the next n bytes. buf is only valid until next call of Next. If an error occurs, buf
4444
// will be nil.
4545
func (r *chunkReader) Next(n int) (buf []byte, err error) {
46+
// Cache buffer pointer to avoid repeated dereferences
47+
b := *r.buf
48+
4649
// Reset the buffer if it is empty
4750
if r.rp == r.wp {
48-
if len(*r.buf) != r.minBufSize {
51+
// Only swap buffer if current one is significantly oversized. Avoids pool thrashing
52+
// when workload alternates between small and large reads. Threshold: 4x minBufSize prevents
53+
// holding onto huge buffers indefinitely while avoiding constant swapping for modest size variations.
54+
if len(b) > r.minBufSize*4 {
4955
iobufpool.Put(r.buf)
5056
r.buf = iobufpool.Get(r.minBufSize)
57+
b = *r.buf
5158
}
5259
r.rp = 0
5360
r.wp = 0
5461
}
5562

5663
// n bytes already in buf
5764
if (r.wp - r.rp) >= n {
58-
buf = (*r.buf)[r.rp : r.rp+n : r.rp+n]
59-
r.rp += n
60-
return buf, err
65+
end := r.rp + n
66+
buf = b[r.rp:end:end]
67+
r.rp = end
68+
return buf, nil
6169
}
6270

6371
// buf is smaller than requested number of bytes
64-
if len(*r.buf) < n {
72+
if len(b) < n {
6573
bigBuf := iobufpool.Get(n)
66-
r.wp = copy((*bigBuf), (*r.buf)[r.rp:r.wp])
74+
r.wp = copy(*bigBuf, b[r.rp:r.wp])
6775
r.rp = 0
6876
iobufpool.Put(r.buf)
6977
r.buf = bigBuf
78+
b = *bigBuf
7079
}
7180

7281
// buf is large enough, but need to shift filled area to start to make enough contiguous space
7382
minReadCount := n - (r.wp - r.rp)
74-
if (len(*r.buf) - r.wp) < minReadCount {
75-
r.wp = copy((*r.buf), (*r.buf)[r.rp:r.wp])
83+
if (len(b) - r.wp) < minReadCount {
84+
r.wp = copy(b, b[r.rp:r.wp])
7685
r.rp = 0
7786
}
7887

7988
// Read at least the required number of bytes from the underlying io.Reader
80-
readBytesCount, err := io.ReadAtLeast(r.r, (*r.buf)[r.wp:], minReadCount)
89+
readBytesCount, err := io.ReadAtLeast(r.r, b[r.wp:], minReadCount)
8190
r.wp += readBytesCount
82-
// fmt.Println("read", n)
8391
if err != nil {
8492
return nil, err
8593
}
8694

87-
buf = (*r.buf)[r.rp : r.rp+n : r.rp+n]
88-
r.rp += n
95+
end := r.rp + n
96+
buf = b[r.rp:end:end]
97+
r.rp = end
8998
return buf, nil
9099
}

0 commit comments

Comments
 (0)