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:
Mike O'Driscoll
2026-03-24 18:08:08 -04:00
committed by GitHub
parent f52c1e3615
commit bb59942df2
2 changed files with 80 additions and 21 deletions
+16 -10
View File
@@ -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
View File
@@ -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)
}
} }
} }