derp,types,util: use bufio Peek+Discard for allocation-free fast reads (#19067)
Replace byte-at-a-time ReadByte loops with Peek+Discard in the DERP
read path. Peek returns a slice into bufio's internal buffer without
allocating, and Discard advances the read pointer without copying.
Introduce util/bufiox with a BufferedReader interface and ReadFull
helper that uses Peek+copy+Discard as an allocation-free alternative
to io.ReadFull.
- derp.ReadFrameHeader: replace 5× ReadByte with Peek(5)+Discard(5),
reading the frame type and length directly from the peeked slice.
Remove now-unused readUint32 helper.
name old ns/op new ns/op speedup
ReadFrameHeader-8 24.2 12.4 ~2x
(0 allocs/op in both)
- key.NodePublic.ReadRawWithoutAllocating: replace 32× ReadByte with
bufiox.ReadFull. Addresses the "Dear future" comment about switching
away from byte-at-a-time reads once a non-escaping alternative exists.
name old ns/op new ns/op speedup
NodeReadRawWithoutAllocating-8 140 43.6 ~3.2x
(0 allocs/op in both)
- derpserver.handleFramePing: replace io.ReadFull with bufiox.ReadFull.
Updates tailscale/corp#38509
Signed-off-by: Mike O'Driscoll <mikeo@tailscale.com>
main
parent
1d0fde6fc2
commit
1403920367
@ -0,0 +1,31 @@ |
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package bufiox provides extensions to the standard bufio package.
|
||||
package bufiox |
||||
|
||||
import "io" |
||||
|
||||
// BufferedReader is an interface for readers that support peeking
|
||||
// into an internal buffer, like [bufio.Reader].
|
||||
type BufferedReader interface { |
||||
Peek(n int) ([]byte, error) |
||||
Discard(n int) (discarded int, err error) |
||||
} |
||||
|
||||
// ReadFull reads exactly len(buf) bytes from r into buf, like
|
||||
// [io.ReadFull], but without heap allocations. It uses Peek to
|
||||
// access the buffered data directly, copies it into buf, then
|
||||
// discards the consumed bytes. If an error occurs,
|
||||
// discard is not called and the buffer is left unchanged.
|
||||
func ReadFull(r BufferedReader, buf []byte) (int, error) { |
||||
b, err := r.Peek(len(buf)) |
||||
if err != nil { |
||||
if len(b) > 0 && err == io.EOF { |
||||
err = io.ErrUnexpectedEOF |
||||
} |
||||
return 0, err |
||||
} |
||||
defer r.Discard(len(buf)) |
||||
return copy(buf, b), nil |
||||
} |
||||
@ -0,0 +1,98 @@ |
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package bufiox |
||||
|
||||
import ( |
||||
"bufio" |
||||
"bytes" |
||||
"io" |
||||
"testing" |
||||
) |
||||
|
||||
func TestReadFull(t *testing.T) { |
||||
data := []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08} |
||||
br := bufio.NewReader(bytes.NewReader(data)) |
||||
|
||||
var buf [5]byte |
||||
n, err := ReadFull(br, buf[:]) |
||||
if err != nil { |
||||
t.Fatalf("ReadFull: %v", err) |
||||
} |
||||
if n != len(buf) { |
||||
t.Fatalf("n = %d, want %d", n, len(buf)) |
||||
} |
||||
if want := [5]byte{0x01, 0x02, 0x03, 0x04, 0x05}; buf != want { |
||||
t.Fatalf("buf = %v, want %v", buf, want) |
||||
} |
||||
|
||||
// Remaining bytes should still be readable.
|
||||
var rest [3]byte |
||||
n, err = ReadFull(br, rest[:]) |
||||
if err != nil { |
||||
t.Fatalf("ReadFull rest: %v", err) |
||||
} |
||||
if n != len(rest) { |
||||
t.Fatalf("rest n = %d, want %d", n, len(rest)) |
||||
} |
||||
if want := [3]byte{0x06, 0x07, 0x08}; rest != want { |
||||
t.Fatalf("rest = %v, want %v", rest, want) |
||||
} |
||||
} |
||||
|
||||
func TestReadFullShort(t *testing.T) { |
||||
data := []byte{0x01, 0x02} |
||||
br := bufio.NewReader(bytes.NewReader(data)) |
||||
|
||||
var buf [5]byte |
||||
_, err := ReadFull(br, buf[:]) |
||||
if err != io.ErrUnexpectedEOF { |
||||
t.Fatalf("err = %v, want %v", err, io.ErrUnexpectedEOF) |
||||
} |
||||
} |
||||
|
||||
func TestReadFullEmpty(t *testing.T) { |
||||
br := bufio.NewReader(bytes.NewReader(nil)) |
||||
|
||||
var buf [1]byte |
||||
_, err := ReadFull(br, buf[:]) |
||||
if err != io.EOF { |
||||
t.Fatalf("err = %v, want %v", err, io.EOF) |
||||
} |
||||
} |
||||
|
||||
func TestReadFullZeroAllocs(t *testing.T) { |
||||
data := make([]byte, 64) |
||||
rd := bytes.NewReader(data) |
||||
br := bufio.NewReader(rd) |
||||
|
||||
var buf [32]byte |
||||
got := testing.AllocsPerRun(1000, func() { |
||||
rd.Reset(data) |
||||
br.Reset(rd) |
||||
_, err := ReadFull(br, buf[:]) |
||||
if err != nil { |
||||
t.Fatalf("ReadFull: %v", err) |
||||
} |
||||
}) |
||||
if got != 0 { |
||||
t.Fatalf("ReadFull allocs = %f, want 0", got) |
||||
} |
||||
} |
||||
|
||||
type nopReader struct{} |
||||
|
||||
func (nopReader) Read(p []byte) (int, error) { return len(p), nil } |
||||
|
||||
func BenchmarkReadFull(b *testing.B) { |
||||
br := bufio.NewReader(nopReader{}) |
||||
var buf [32]byte |
||||
b.ReportAllocs() |
||||
b.ResetTimer() |
||||
for range b.N { |
||||
_, err := ReadFull(br, buf[:]) |
||||
if err != nil { |
||||
b.Fatal(err) |
||||
} |
||||
} |
||||
} |
||||
Loading…
Reference in new issue