From e671fe0c3efc497397af3362a4b79c895fbd8bfc Mon Sep 17 00:00:00 2001 From: Joe Tsai Date: Mon, 6 Feb 2023 11:37:39 -0800 Subject: bytes: add Buffer.Available and Buffer.AvailableBuffer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This adds a new Buffer.AvailableBuffer method that returns an empty buffer with a possibly non-empty capacity for use with append-like APIs. The typical usage pattern is something like: b := bb.AvailableBuffer() b = appendValue(b, v) bb.Write(b) It allows logic combining append-like APIs with Buffer to avoid needing to allocate and manage buffers themselves and allows the append-like APIs to directly write into the Buffer. The Buffer.Write method uses the builtin copy function, which avoids copying bytes if the source and destination are identical. Thus, Buffer.Write is a constant-time call for this pattern. Performance: BenchmarkBufferAppendNoCopy 2.909 ns/op 5766942167.24 MB/s This benchmark should only be testing the cost of bookkeeping and never the copying of the input slice. Thus, the MB/s should be orders of magnitude faster than RAM. Fixes #53685 Change-Id: I0b41e54361339df309db8d03527689b123f99085 Reviewed-on: https://go-review.googlesource.com/c/go/+/474635 Run-TryBot: Joseph Tsai Reviewed-by: Daniel Martí Reviewed-by: Cherry Mui TryBot-Result: Gopher Robot Auto-Submit: Joseph Tsai Reviewed-by: Ian Lance Taylor --- src/bytes/buffer_test.go | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) (limited to 'src/bytes/buffer_test.go') diff --git a/src/bytes/buffer_test.go b/src/bytes/buffer_test.go index c0855007c1..81476fbae1 100644 --- a/src/bytes/buffer_test.go +++ b/src/bytes/buffer_test.go @@ -9,6 +9,7 @@ import ( "fmt" "io" "math/rand" + "strconv" "testing" "unicode/utf8" ) @@ -326,6 +327,33 @@ func TestWriteTo(t *testing.T) { } } +func TestWriteAppend(t *testing.T) { + var got Buffer + var want []byte + for i := 0; i < 1000; i++ { + b := got.AvailableBuffer() + b = strconv.AppendInt(b, int64(i), 10) + want = strconv.AppendInt(want, int64(i), 10) + got.Write(b) + } + if !Equal(got.Bytes(), want) { + t.Fatalf("Bytes() = %q, want %q", got, want) + } + + // With a sufficiently sized buffer, there should be no allocations. + n := testing.AllocsPerRun(100, func() { + got.Reset() + for i := 0; i < 1000; i++ { + b := got.AvailableBuffer() + b = strconv.AppendInt(b, int64(i), 10) + got.Write(b) + } + }) + if n > 0 { + t.Errorf("allocations occurred while appending") + } +} + func TestRuneIO(t *testing.T) { const NRune = 1000 // Built a test slice while we write the data @@ -687,3 +715,16 @@ func BenchmarkBufferWriteBlock(b *testing.B) { }) } } + +func BenchmarkBufferAppendNoCopy(b *testing.B) { + var bb Buffer + bb.Grow(16 << 20) + b.SetBytes(int64(bb.Available())) + b.ReportAllocs() + for i := 0; i < b.N; i++ { + bb.Reset() + b := bb.AvailableBuffer() + b = b[:cap(b)] // use max capacity to simulate a large append operation + bb.Write(b) // should be nearly infinitely fast + } +} -- cgit v1.3