bytes: add Buffer.Available and Buffer.AvailableBuffer

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 <joetsai@digital-static.net>
Reviewed-by: Daniel Martí <mvdan@mvdan.cc>
Reviewed-by: Cherry Mui <cherryyz@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
Auto-Submit: Joseph Tsai <joetsai@digital-static.net>
Reviewed-by: Ian Lance Taylor <iant@google.com>
This commit is contained in:
Joe Tsai 2023-02-06 11:37:39 -08:00 committed by Gopher Robot
parent bcd8161f4e
commit e671fe0c3e
4 changed files with 65 additions and 0 deletions

View file

@ -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
}
}