Skip to content

Commit 2d026a4

Browse files
committed
bytes: rely on runtime.growslice for growing
Rather than naively making a slice of capacity 2*c+n, rely on the append(..., make(...)) pattern to allocate a slice that aligns up to the closest size class. Performance: name old time/op new time/op delta BufferWriteBlock/N4096 3.03µs ± 6% 2.04µs ± 6% -32.60% (p=0.000 n=10+10) BufferWriteBlock/N65536 47.8µs ± 6% 28.1µs ± 2% -41.32% (p=0.000 n=9+8) BufferWriteBlock/N1048576 844µs ± 7% 510µs ± 5% -39.59% (p=0.000 n=8+9) name old alloc/op new alloc/op delta BufferWriteBlock/N4096 12.3kB ± 0% 7.2kB ± 0% -41.67% (p=0.000 n=10+10) BufferWriteBlock/N65536 258kB ± 0% 130kB ± 0% -49.60% (p=0.000 n=10+10) BufferWriteBlock/N1048576 4.19MB ± 0% 2.10MB ± 0% -49.98% (p=0.000 n=10+8) name old allocs/op new allocs/op delta BufferWriteBlock/N4096 3.00 ± 0% 3.00 ± 0% ~ (all equal) BufferWriteBlock/N65536 7.00 ± 0% 7.00 ± 0% ~ (all equal) BufferWriteBlock/N1048576 11.0 ± 0% 11.0 ± 0% ~ (all equal) The performance is faster since the growth rate is capped at 2x, while previously it could grow by amounts potentially much greater than 2x, leading to significant amounts of memory waste and extra copying. Credit goes to Martin Möhrmann for suggesting the append(b, make([]T, n)...) pattern. Fixes #42984 Updates #51462 Change-Id: I7b23f75dddbf53f8b8b93485bb1a1fff9649b96b Reviewed-on: https://go-review.googlesource.com/c/go/+/349994 Trust: Joseph Tsai <[email protected]> Trust: Josh Bleecher Snyder <[email protected]> Reviewed-by: Bryan Mills <[email protected]> Reviewed-by: Ian Lance Taylor <[email protected]> Reviewed-by: Josh Bleecher Snyder <[email protected]>
1 parent 1cf6770 commit 2d026a4

File tree

2 files changed

+37
-9
lines changed

2 files changed

+37
-9
lines changed

src/bytes/buffer.go

+22-9
Original file line numberDiff line numberDiff line change
@@ -138,10 +138,8 @@ func (b *Buffer) grow(n int) int {
138138
} else if c > maxInt-c-n {
139139
panic(ErrTooLarge)
140140
} else {
141-
// Not enough space anywhere, we need to allocate.
142-
buf := makeSlice(2*c + n)
143-
copy(buf, b.buf[b.off:])
144-
b.buf = buf
141+
// Add b.off to account for b.buf[:b.off] being sliced off the front.
142+
b.buf = growSlice(b.buf[b.off:], b.off+n)
145143
}
146144
// Restore b.off and len(b.buf).
147145
b.off = 0
@@ -217,16 +215,31 @@ func (b *Buffer) ReadFrom(r io.Reader) (n int64, err error) {
217215
}
218216
}
219217

220-
// makeSlice allocates a slice of size n. If the allocation fails, it panics
221-
// with ErrTooLarge.
222-
func makeSlice(n int) []byte {
223-
// If the make fails, give a known error.
218+
// growSlice grows b by n, preserving the original content of b.
219+
// If the allocation fails, it panics with ErrTooLarge.
220+
func growSlice(b []byte, n int) []byte {
224221
defer func() {
225222
if recover() != nil {
226223
panic(ErrTooLarge)
227224
}
228225
}()
229-
return make([]byte, n)
226+
// TODO(http://golang.org/issue/51462): We should rely on the append-make
227+
// pattern so that the compiler can call runtime.growslice. For example:
228+
// return append(b, make([]byte, n)...)
229+
// This avoids unnecessary zero-ing of the first len(b) bytes of the
230+
// allocated slice, but this pattern causes b to escape onto the heap.
231+
//
232+
// Instead use the append-make pattern with a nil slice to ensure that
233+
// we allocate buffers rounded up to the closest size class.
234+
c := len(b) + n // ensure enough space for n elements
235+
if c < 2*cap(b) {
236+
// The growth rate has historically always been 2x. In the future,
237+
// we could rely purely on append to determine the growth rate.
238+
c = 2 * cap(b)
239+
}
240+
b2 := append([]byte(nil), make([]byte, c)...)
241+
copy(b2, b)
242+
return b2[:len(b)]
230243
}
231244

232245
// WriteTo writes data to w until the buffer is drained or an error occurs.

src/bytes/buffer_test.go

+15
Original file line numberDiff line numberDiff line change
@@ -672,3 +672,18 @@ func BenchmarkBufferFullSmallReads(b *testing.B) {
672672
}
673673
}
674674
}
675+
676+
func BenchmarkBufferWriteBlock(b *testing.B) {
677+
block := make([]byte, 1024)
678+
for _, n := range []int{1 << 12, 1 << 16, 1 << 20} {
679+
b.Run(fmt.Sprintf("N%d", n), func(b *testing.B) {
680+
b.ReportAllocs()
681+
for i := 0; i < b.N; i++ {
682+
var bb Buffer
683+
for bb.Len() < n {
684+
bb.Write(block)
685+
}
686+
}
687+
})
688+
}
689+
}

0 commit comments

Comments
 (0)