types/key: use AvailableBuffer for WriteRawWithoutAllocating (#19102)
Use bufio.Writer.AvailableBuffer to write the 32-byte public key directly into bufio's internal buffer as a single append+Write, avoiding 32 separate WriteByte calls. Fall back to the existing byte-at-a-time path when the buffer has insufficient space. ``` name old ns/op new ns/op speedup NodeWriteRawWithoutAllocating-8 121 12.5 ~9.7x (0 allocs/op in both) ``` Add BenchmarkNodeWriteRawWithoutAllocating and expand TestNodeWriteRawWithoutAllocating to cover both fast (AvailableBuffer) and slow (WriteByte fallback) paths with correctness and allocation checks. Updates tailscale/corp#38509 Signed-off-by: Mike O'Driscoll <mikeo@tailscale.com>
This commit is contained in:
+16
-10
@@ -253,18 +253,24 @@ func (k *NodePublic) ReadRawWithoutAllocating(br *bufio.Reader) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// WriteRawWithoutAllocating writes out k as 32 bytes to bw.
|
// WriteRawWithoutAllocating writes out k as 32 big-endian bytes to bw.
|
||||||
// The writing is done ~3x slower than bw.Write, but in exchange is
|
//
|
||||||
// allocation-free.
|
// It uses AvailableBuffer to append directly into bufio's internal
|
||||||
|
// buffer without allocation, falling back to WriteByte when the
|
||||||
|
// buffer has insufficient space.
|
||||||
func (k NodePublic) WriteRawWithoutAllocating(bw *bufio.Writer) error {
|
func (k NodePublic) WriteRawWithoutAllocating(bw *bufio.Writer) error {
|
||||||
// Equivalent to bw.Write(k.k[:]), but without causing an
|
// Fast path: enough space in the buffer to append directly.
|
||||||
// escape-related alloc.
|
if bw.Available() >= len(k.k) {
|
||||||
//
|
buf := bw.AvailableBuffer()
|
||||||
// Dear future: if bw.Write(k.k[:]) stops causing stuff to escape,
|
buf = append(buf, k.k[:]...)
|
||||||
// you should switch back to that.
|
_, err := bw.Write(buf)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// Slow path: buffer nearly full. Write byte-at-a-time to let
|
||||||
|
// bufio flush as needed, avoiding a heap allocation from append
|
||||||
|
// growing past AvailableBuffer's capacity.
|
||||||
for _, b := range k.k {
|
for _, b := range k.k {
|
||||||
err := bw.WriteByte(b)
|
if err := bw.WriteByte(b); err != nil {
|
||||||
if err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+64
-11
@@ -7,6 +7,7 @@ import (
|
|||||||
"bufio"
|
"bufio"
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"io"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
@@ -133,8 +134,7 @@ func BenchmarkNodeReadRawWithoutAllocating(b *testing.B) {
|
|||||||
r := bytes.NewReader(buf)
|
r := bytes.NewReader(buf)
|
||||||
br := bufio.NewReader(r)
|
br := bufio.NewReader(r)
|
||||||
b.ReportAllocs()
|
b.ReportAllocs()
|
||||||
b.ResetTimer()
|
for b.Loop() {
|
||||||
for range b.N {
|
|
||||||
r.Reset(buf)
|
r.Reset(buf)
|
||||||
br.Reset(r)
|
br.Reset(r)
|
||||||
var k NodePublic
|
var k NodePublic
|
||||||
@@ -145,19 +145,72 @@ func BenchmarkNodeReadRawWithoutAllocating(b *testing.B) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestNodeWriteRawWithoutAllocating(t *testing.T) {
|
func TestNodeWriteRawWithoutAllocating(t *testing.T) {
|
||||||
buf := make([]byte, 0, 32)
|
var k NodePublic
|
||||||
w := bytes.NewBuffer(buf)
|
for i := range k.k {
|
||||||
bw := bufio.NewWriter(w)
|
k.k[i] = byte(i)
|
||||||
got := testing.AllocsPerRun(1000, func() {
|
}
|
||||||
w.Reset()
|
|
||||||
bw.Reset(w)
|
// Test fast path (empty buffer, plenty of space).
|
||||||
var k NodePublic
|
t.Run("fast", func(t *testing.T) {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
bw := bufio.NewWriter(&buf)
|
||||||
if err := k.WriteRawWithoutAllocating(bw); err != nil {
|
if err := k.WriteRawWithoutAllocating(bw); err != nil {
|
||||||
t.Fatalf("WriteRawWithoutAllocating: %v", err)
|
t.Fatalf("WriteRawWithoutAllocating: %v", err)
|
||||||
}
|
}
|
||||||
|
bw.Flush()
|
||||||
|
if got := buf.Bytes(); !bytes.Equal(got, k.k[:]) {
|
||||||
|
t.Errorf("wrote % 02x, want % 02x", got, k.k)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
if want := 0.0; got != want {
|
|
||||||
t.Fatalf("WriteRawWithoutAllocating got %f allocs, want %f", got, want)
|
// Test slow path (buffer nearly full, less than 32 bytes available).
|
||||||
|
t.Run("slow", func(t *testing.T) {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
const smallBuf = 40
|
||||||
|
bw := bufio.NewWriterSize(&buf, smallBuf)
|
||||||
|
// Fill buffer to leave less than 32 bytes available.
|
||||||
|
padding := make([]byte, smallBuf-len(k.k)+1)
|
||||||
|
if _, err := bw.Write(padding); err != nil {
|
||||||
|
t.Fatalf("Write padding: %v", err)
|
||||||
|
}
|
||||||
|
if err := k.WriteRawWithoutAllocating(bw); err != nil {
|
||||||
|
t.Fatalf("WriteRawWithoutAllocating: %v", err)
|
||||||
|
}
|
||||||
|
bw.Flush()
|
||||||
|
got := buf.Bytes()[len(padding):]
|
||||||
|
if !bytes.Equal(got, k.k[:]) {
|
||||||
|
t.Errorf("wrote % 02x, want % 02x", got, k.k)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Verify zero allocations on fast path.
|
||||||
|
t.Run("allocs", func(t *testing.T) {
|
||||||
|
w := bytes.NewBuffer(make([]byte, 0, 32))
|
||||||
|
bw := bufio.NewWriter(w)
|
||||||
|
got := testing.AllocsPerRun(1000, func() {
|
||||||
|
w.Reset()
|
||||||
|
bw.Reset(w)
|
||||||
|
if err := k.WriteRawWithoutAllocating(bw); err != nil {
|
||||||
|
t.Fatalf("WriteRawWithoutAllocating: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if got != 0 {
|
||||||
|
t.Fatalf("WriteRawWithoutAllocating allocs = %f, want 0", got)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkNodeWriteRawWithoutAllocating(b *testing.B) {
|
||||||
|
bw := bufio.NewWriter(io.Discard)
|
||||||
|
var k NodePublic
|
||||||
|
for i := range k.k {
|
||||||
|
k.k[i] = 0x42
|
||||||
|
}
|
||||||
|
b.ReportAllocs()
|
||||||
|
for b.Loop() {
|
||||||
|
if err := k.WriteRawWithoutAllocating(bw); err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user