Improve performance/reduce allocs of bytespipe
Creates a `fixedBuffer` type that is used to encapsulate functionality for reading/writing from the underlying byte slices. Uses lazily-loaded set of sync.Pools for storing buffers that are no longer needed so they can be re-used. ``` benchmark old ns/op new ns/op delta BenchmarkBytesPipeWrite-8 138469 48985 -64.62% BenchmarkBytesPipeRead-8 130922 56601 -56.77% benchmark old allocs new allocs delta BenchmarkBytesPipeWrite-8 18 8 -55.56% BenchmarkBytesPipeRead-8 0 0 +0.00% benchmark old bytes new bytes delta BenchmarkBytesPipeWrite-8 66903 1649 -97.54% BenchmarkBytesPipeRead-8 0 1 +Inf% ``` Signed-off-by: Brian Goff <cpuguy83@gmail.com>
This commit is contained in:
parent
e2664472b6
commit
729c6a44bc
4 changed files with 208 additions and 57 deletions
51
ioutils/buffer.go
Normal file
51
ioutils/buffer.go
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
package ioutils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
var errBufferFull = errors.New("buffer is full")
|
||||||
|
|
||||||
|
type fixedBuffer struct {
|
||||||
|
buf []byte
|
||||||
|
pos int
|
||||||
|
lastRead int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *fixedBuffer) Write(p []byte) (int, error) {
|
||||||
|
n := copy(b.buf[b.pos:cap(b.buf)], p)
|
||||||
|
b.pos += n
|
||||||
|
|
||||||
|
if n < len(p) {
|
||||||
|
if b.pos == cap(b.buf) {
|
||||||
|
return n, errBufferFull
|
||||||
|
}
|
||||||
|
return n, io.ErrShortWrite
|
||||||
|
}
|
||||||
|
return n, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *fixedBuffer) Read(p []byte) (int, error) {
|
||||||
|
n := copy(p, b.buf[b.lastRead:b.pos])
|
||||||
|
b.lastRead += n
|
||||||
|
return n, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *fixedBuffer) Len() int {
|
||||||
|
return b.pos - b.lastRead
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *fixedBuffer) Cap() int {
|
||||||
|
return cap(b.buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *fixedBuffer) Reset() {
|
||||||
|
b.pos = 0
|
||||||
|
b.lastRead = 0
|
||||||
|
b.buf = b.buf[:0]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *fixedBuffer) String() string {
|
||||||
|
return string(b.buf[b.lastRead:b.pos])
|
||||||
|
}
|
75
ioutils/buffer_test.go
Normal file
75
ioutils/buffer_test.go
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
package ioutils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFixedBufferWrite(t *testing.T) {
|
||||||
|
buf := &fixedBuffer{buf: make([]byte, 0, 64)}
|
||||||
|
n, err := buf.Write([]byte("hello"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if n != 5 {
|
||||||
|
t.Fatalf("expected 5 bytes written, got %d", n)
|
||||||
|
}
|
||||||
|
|
||||||
|
if string(buf.buf[:5]) != "hello" {
|
||||||
|
t.Fatalf("expected \"hello\", got %q", string(buf.buf[:5]))
|
||||||
|
}
|
||||||
|
|
||||||
|
n, err = buf.Write(bytes.Repeat([]byte{1}, 64))
|
||||||
|
if err != errBufferFull {
|
||||||
|
t.Fatalf("expected errBufferFull, got %v - %v", err, buf.buf[:64])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFixedBufferRead(t *testing.T) {
|
||||||
|
buf := &fixedBuffer{buf: make([]byte, 0, 64)}
|
||||||
|
if _, err := buf.Write([]byte("hello world")); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
b := make([]byte, 5)
|
||||||
|
n, err := buf.Read(b)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if n != 5 {
|
||||||
|
t.Fatalf("expected 5 bytes read, got %d - %s", n, buf.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
if string(b) != "hello" {
|
||||||
|
t.Fatalf("expected \"hello\", got %q", string(b))
|
||||||
|
}
|
||||||
|
|
||||||
|
n, err = buf.Read(b)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if n != 5 {
|
||||||
|
t.Fatalf("expected 5 bytes read, got %d", n)
|
||||||
|
}
|
||||||
|
|
||||||
|
if string(b) != " worl" {
|
||||||
|
t.Fatalf("expected \" worl\", got %s", string(b))
|
||||||
|
}
|
||||||
|
|
||||||
|
b = b[:1]
|
||||||
|
n, err = buf.Read(b)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if n != 1 {
|
||||||
|
t.Fatalf("expected 1 byte read, got %d - %s", n, buf.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
if string(b) != "d" {
|
||||||
|
t.Fatalf("expected \"d\", got %s", string(b))
|
||||||
|
}
|
||||||
|
}
|
|
@ -9,12 +9,19 @@ import (
|
||||||
// maxCap is the highest capacity to use in byte slices that buffer data.
|
// maxCap is the highest capacity to use in byte slices that buffer data.
|
||||||
const maxCap = 1e6
|
const maxCap = 1e6
|
||||||
|
|
||||||
|
// minCap is the lowest capacity to use in byte slices that buffer data
|
||||||
|
const minCap = 64
|
||||||
|
|
||||||
// blockThreshold is the minimum number of bytes in the buffer which will cause
|
// blockThreshold is the minimum number of bytes in the buffer which will cause
|
||||||
// a write to BytesPipe to block when allocating a new slice.
|
// a write to BytesPipe to block when allocating a new slice.
|
||||||
const blockThreshold = 1e6
|
const blockThreshold = 1e6
|
||||||
|
|
||||||
// ErrClosed is returned when Write is called on a closed BytesPipe.
|
var (
|
||||||
var ErrClosed = errors.New("write to closed BytesPipe")
|
// ErrClosed is returned when Write is called on a closed BytesPipe.
|
||||||
|
ErrClosed = errors.New("write to closed BytesPipe")
|
||||||
|
|
||||||
|
bufPools = make(map[int]*sync.Pool)
|
||||||
|
)
|
||||||
|
|
||||||
// BytesPipe is io.ReadWriteCloser which works similarly to pipe(queue).
|
// BytesPipe is io.ReadWriteCloser which works similarly to pipe(queue).
|
||||||
// All written data may be read at most once. Also, BytesPipe allocates
|
// All written data may be read at most once. Also, BytesPipe allocates
|
||||||
|
@ -23,22 +30,17 @@ var ErrClosed = errors.New("write to closed BytesPipe")
|
||||||
type BytesPipe struct {
|
type BytesPipe struct {
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
wait *sync.Cond
|
wait *sync.Cond
|
||||||
buf [][]byte // slice of byte-slices of buffered data
|
buf []*fixedBuffer
|
||||||
lastRead int // index in the first slice to a read point
|
bufLen int
|
||||||
bufLen int // length of data buffered over the slices
|
closeErr error // error to return from next Read. set to nil if not closed.
|
||||||
closeErr error // error to return from next Read. set to nil if not closed.
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewBytesPipe creates new BytesPipe, initialized by specified slice.
|
// NewBytesPipe creates new BytesPipe, initialized by specified slice.
|
||||||
// If buf is nil, then it will be initialized with slice which cap is 64.
|
// If buf is nil, then it will be initialized with slice which cap is 64.
|
||||||
// buf will be adjusted in a way that len(buf) == 0, cap(buf) == cap(buf).
|
// buf will be adjusted in a way that len(buf) == 0, cap(buf) == cap(buf).
|
||||||
func NewBytesPipe(buf []byte) *BytesPipe {
|
func NewBytesPipe() *BytesPipe {
|
||||||
if cap(buf) == 0 {
|
bp := &BytesPipe{}
|
||||||
buf = make([]byte, 0, 64)
|
bp.buf = append(bp.buf, getBuffer(minCap))
|
||||||
}
|
|
||||||
bp := &BytesPipe{
|
|
||||||
buf: [][]byte{buf[:0]},
|
|
||||||
}
|
|
||||||
bp.wait = sync.NewCond(&bp.mu)
|
bp.wait = sync.NewCond(&bp.mu)
|
||||||
return bp
|
return bp
|
||||||
}
|
}
|
||||||
|
@ -47,22 +49,30 @@ func NewBytesPipe(buf []byte) *BytesPipe {
|
||||||
// It can allocate new []byte slices in a process of writing.
|
// It can allocate new []byte slices in a process of writing.
|
||||||
func (bp *BytesPipe) Write(p []byte) (int, error) {
|
func (bp *BytesPipe) Write(p []byte) (int, error) {
|
||||||
bp.mu.Lock()
|
bp.mu.Lock()
|
||||||
defer bp.mu.Unlock()
|
|
||||||
written := 0
|
written := 0
|
||||||
for {
|
for {
|
||||||
if bp.closeErr != nil {
|
if bp.closeErr != nil {
|
||||||
|
bp.mu.Unlock()
|
||||||
return written, ErrClosed
|
return written, ErrClosed
|
||||||
}
|
}
|
||||||
// write data to the last buffer
|
|
||||||
b := bp.buf[len(bp.buf)-1]
|
|
||||||
// copy data to the current empty allocated area
|
|
||||||
n := copy(b[len(b):cap(b)], p)
|
|
||||||
// increment buffered data length
|
|
||||||
bp.bufLen += n
|
|
||||||
// include written data in last buffer
|
|
||||||
bp.buf[len(bp.buf)-1] = b[:len(b)+n]
|
|
||||||
|
|
||||||
|
if len(bp.buf) == 0 {
|
||||||
|
bp.buf = append(bp.buf, getBuffer(64))
|
||||||
|
}
|
||||||
|
// get the last buffer
|
||||||
|
b := bp.buf[len(bp.buf)-1]
|
||||||
|
|
||||||
|
n, err := b.Write(p)
|
||||||
written += n
|
written += n
|
||||||
|
bp.bufLen += n
|
||||||
|
|
||||||
|
// errBufferFull is an error we expect to get if the buffer is full
|
||||||
|
if err != nil && err != errBufferFull {
|
||||||
|
bp.wait.Broadcast()
|
||||||
|
bp.mu.Unlock()
|
||||||
|
return written, err
|
||||||
|
}
|
||||||
|
|
||||||
// if there was enough room to write all then break
|
// if there was enough room to write all then break
|
||||||
if len(p) == n {
|
if len(p) == n {
|
||||||
|
@ -72,20 +82,20 @@ func (bp *BytesPipe) Write(p []byte) (int, error) {
|
||||||
// more data: write to the next slice
|
// more data: write to the next slice
|
||||||
p = p[n:]
|
p = p[n:]
|
||||||
|
|
||||||
// block if too much data is still in the buffer
|
// make sure the buffer doesn't grow too big from this write
|
||||||
for bp.bufLen >= blockThreshold {
|
for bp.bufLen >= blockThreshold {
|
||||||
bp.wait.Wait()
|
bp.wait.Wait()
|
||||||
}
|
}
|
||||||
|
|
||||||
// allocate slice that has twice the size of the last unless maximum reached
|
// add new byte slice to the buffers slice and continue writing
|
||||||
nextCap := 2 * cap(bp.buf[len(bp.buf)-1])
|
nextCap := b.Cap() * 2
|
||||||
if nextCap > maxCap {
|
if nextCap > maxCap {
|
||||||
nextCap = maxCap
|
nextCap = maxCap
|
||||||
}
|
}
|
||||||
// add new byte slice to the buffers slice and continue writing
|
bp.buf = append(bp.buf, getBuffer(nextCap))
|
||||||
bp.buf = append(bp.buf, make([]byte, 0, nextCap))
|
|
||||||
}
|
}
|
||||||
bp.wait.Broadcast()
|
bp.wait.Broadcast()
|
||||||
|
bp.mu.Unlock()
|
||||||
return written, nil
|
return written, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -107,46 +117,60 @@ func (bp *BytesPipe) Close() error {
|
||||||
return bp.CloseWithError(nil)
|
return bp.CloseWithError(nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (bp *BytesPipe) len() int {
|
|
||||||
return bp.bufLen - bp.lastRead
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read reads bytes from BytesPipe.
|
// Read reads bytes from BytesPipe.
|
||||||
// Data could be read only once.
|
// Data could be read only once.
|
||||||
func (bp *BytesPipe) Read(p []byte) (n int, err error) {
|
func (bp *BytesPipe) Read(p []byte) (n int, err error) {
|
||||||
bp.mu.Lock()
|
bp.mu.Lock()
|
||||||
defer bp.mu.Unlock()
|
if bp.bufLen == 0 {
|
||||||
if bp.len() == 0 {
|
|
||||||
if bp.closeErr != nil {
|
if bp.closeErr != nil {
|
||||||
|
bp.mu.Unlock()
|
||||||
return 0, bp.closeErr
|
return 0, bp.closeErr
|
||||||
}
|
}
|
||||||
bp.wait.Wait()
|
bp.wait.Wait()
|
||||||
if bp.len() == 0 && bp.closeErr != nil {
|
if bp.bufLen == 0 && bp.closeErr != nil {
|
||||||
|
bp.mu.Unlock()
|
||||||
return 0, bp.closeErr
|
return 0, bp.closeErr
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for {
|
|
||||||
read := copy(p, bp.buf[0][bp.lastRead:])
|
for bp.bufLen > 0 {
|
||||||
|
b := bp.buf[0]
|
||||||
|
read, _ := b.Read(p) // ignore error since fixedBuffer doesn't really return an error
|
||||||
n += read
|
n += read
|
||||||
bp.lastRead += read
|
bp.bufLen -= read
|
||||||
if bp.len() == 0 {
|
|
||||||
// we have read everything. reset to the beginning.
|
if b.Len() == 0 {
|
||||||
bp.lastRead = 0
|
// it's empty so return it to the pool and move to the next one
|
||||||
bp.bufLen -= len(bp.buf[0])
|
returnBuffer(b)
|
||||||
bp.buf[0] = bp.buf[0][:0]
|
bp.buf[0] = nil
|
||||||
break
|
bp.buf = bp.buf[1:]
|
||||||
}
|
}
|
||||||
// break if everything was read
|
|
||||||
if len(p) == read {
|
if len(p) == read {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
// more buffered data and more asked. read from next slice.
|
|
||||||
p = p[read:]
|
p = p[read:]
|
||||||
bp.lastRead = 0
|
|
||||||
bp.bufLen -= len(bp.buf[0])
|
|
||||||
bp.buf[0] = nil // throw away old slice
|
|
||||||
bp.buf = bp.buf[1:] // switch to next
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bp.wait.Broadcast()
|
bp.wait.Broadcast()
|
||||||
|
bp.mu.Unlock()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func returnBuffer(b *fixedBuffer) {
|
||||||
|
b.Reset()
|
||||||
|
pool := bufPools[b.Cap()]
|
||||||
|
if pool != nil {
|
||||||
|
pool.Put(b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getBuffer(size int) *fixedBuffer {
|
||||||
|
pool, ok := bufPools[size]
|
||||||
|
if !ok {
|
||||||
|
pool = &sync.Pool{New: func() interface{} { return &fixedBuffer{buf: make([]byte, 0, size)} }}
|
||||||
|
bufPools[size] = pool
|
||||||
|
}
|
||||||
|
return pool.Get().(*fixedBuffer)
|
||||||
|
}
|
||||||
|
|
|
@ -9,7 +9,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestBytesPipeRead(t *testing.T) {
|
func TestBytesPipeRead(t *testing.T) {
|
||||||
buf := NewBytesPipe(nil)
|
buf := NewBytesPipe()
|
||||||
buf.Write([]byte("12"))
|
buf.Write([]byte("12"))
|
||||||
buf.Write([]byte("34"))
|
buf.Write([]byte("34"))
|
||||||
buf.Write([]byte("56"))
|
buf.Write([]byte("56"))
|
||||||
|
@ -49,14 +49,14 @@ func TestBytesPipeRead(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBytesPipeWrite(t *testing.T) {
|
func TestBytesPipeWrite(t *testing.T) {
|
||||||
buf := NewBytesPipe(nil)
|
buf := NewBytesPipe()
|
||||||
buf.Write([]byte("12"))
|
buf.Write([]byte("12"))
|
||||||
buf.Write([]byte("34"))
|
buf.Write([]byte("34"))
|
||||||
buf.Write([]byte("56"))
|
buf.Write([]byte("56"))
|
||||||
buf.Write([]byte("78"))
|
buf.Write([]byte("78"))
|
||||||
buf.Write([]byte("90"))
|
buf.Write([]byte("90"))
|
||||||
if string(buf.buf[0]) != "1234567890" {
|
if buf.buf[0].String() != "1234567890" {
|
||||||
t.Fatalf("Buffer %s, must be %s", buf.buf, "1234567890")
|
t.Fatalf("Buffer %q, must be %q", buf.buf[0].String(), "1234567890")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -86,7 +86,7 @@ func TestBytesPipeWriteRandomChunks(t *testing.T) {
|
||||||
expected := hex.EncodeToString(hash.Sum(nil))
|
expected := hex.EncodeToString(hash.Sum(nil))
|
||||||
|
|
||||||
// write/read through buffer
|
// write/read through buffer
|
||||||
buf := NewBytesPipe(nil)
|
buf := NewBytesPipe()
|
||||||
hash.Reset()
|
hash.Reset()
|
||||||
|
|
||||||
done := make(chan struct{})
|
done := make(chan struct{})
|
||||||
|
@ -124,9 +124,10 @@ func TestBytesPipeWriteRandomChunks(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func BenchmarkBytesPipeWrite(b *testing.B) {
|
func BenchmarkBytesPipeWrite(b *testing.B) {
|
||||||
|
testData := []byte("pretty short line, because why not?")
|
||||||
for i := 0; i < b.N; i++ {
|
for i := 0; i < b.N; i++ {
|
||||||
readBuf := make([]byte, 1024)
|
readBuf := make([]byte, 1024)
|
||||||
buf := NewBytesPipe(nil)
|
buf := NewBytesPipe()
|
||||||
go func() {
|
go func() {
|
||||||
var err error
|
var err error
|
||||||
for err == nil {
|
for err == nil {
|
||||||
|
@ -134,7 +135,7 @@ func BenchmarkBytesPipeWrite(b *testing.B) {
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
for j := 0; j < 1000; j++ {
|
for j := 0; j < 1000; j++ {
|
||||||
buf.Write([]byte("pretty short line, because why not?"))
|
buf.Write(testData)
|
||||||
}
|
}
|
||||||
buf.Close()
|
buf.Close()
|
||||||
}
|
}
|
||||||
|
@ -144,7 +145,7 @@ func BenchmarkBytesPipeRead(b *testing.B) {
|
||||||
rd := make([]byte, 512)
|
rd := make([]byte, 512)
|
||||||
for i := 0; i < b.N; i++ {
|
for i := 0; i < b.N; i++ {
|
||||||
b.StopTimer()
|
b.StopTimer()
|
||||||
buf := NewBytesPipe(nil)
|
buf := NewBytesPipe()
|
||||||
for j := 0; j < 500; j++ {
|
for j := 0; j < 500; j++ {
|
||||||
buf.Write(make([]byte, 1024))
|
buf.Write(make([]byte, 1024))
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue