Skip to content

Commit

Permalink
Improve read speed (#361)
Browse files Browse the repository at this point in the history
* Improve read speed

Utilize philhofer/fwd#32 to peek bytes

Eliminate bounds checks, mostly on reading sizes, but also integers.

```
benchmark                            old MB/s     new MB/s     speedup
BenchmarkReadMapHeaderBytes-32       1136.75      1136.89      1.00x
BenchmarkReadArrayHeaderBytes-32     1133.52      1149.34      1.01x
BenchmarkTestReadBytesHeader-32      1200.18      1171.21      0.98x
BenchmarkReadNilByte-32              2907.50      2176.63      0.75x
BenchmarkReadFloat64Bytes-32         4308.86      3869.64      0.90x
BenchmarkReadFloat32Bytes-32         2354.55      2332.21      0.99x
BenchmarkReadBoolBytes-32            863.66       1124.07      1.30x
BenchmarkReadTimeBytes-32            3710.27      3668.07      0.99x
BenchmarkReadMapHeader-32            228.74       391.76       1.71x
BenchmarkReadArrayHeader-32          243.90       454.31       1.86x
BenchmarkReadNil-32                  127.42       191.55       1.50x
BenchmarkReadFloat64-32              1046.08      1142.49      1.09x
BenchmarkReadFloat32-32              595.67       669.65       1.12x
BenchmarkReadInt64-32                422.29       798.35       1.89x
BenchmarkReadUintWithInt64-32        186.59       371.28       1.99x
BenchmarkReadUint64-32               282.87       377.76       1.34x
BenchmarkReadIntWithUint64-32        465.30       684.49       1.47x
BenchmarkRead16Bytes-32              1021.15      1021.38      1.00x
BenchmarkRead256Bytes-32             5324.42      6166.06      1.16x
BenchmarkRead2048Bytes-32            8405.41      8382.61      1.00x
BenchmarkRead16StringAsBytes-32      1102.72      1321.71      1.20x
BenchmarkRead256StringAsBytes-32     6959.78      6627.49      0.95x
BenchmarkRead16String-32             320.75       378.46       1.18x
BenchmarkRead256String-32            2066.40      2242.78      1.09x
BenchmarkReadComplex64-32            900.13       1064.21      1.18x
BenchmarkReadComplex128-32           1809.88      2028.11      1.12x
BenchmarkReadTime-32                 1362.46      1425.79      1.05x
```

Apologies for the noisy benchmark. `BenchmarkReadNilByte` and `BenchmarkReadFloat64Byte` does not touch new code, so it seems to be "micro-bench-itis", where random deltas show up. Bench takes ~30m to run for whatever reason.

The compiler is not clever enough to track back to `l := len(b)`, so it inserts a bounds check. Also `len(b) < 3` and `big.Uint16(b[1:])` causes a bounds check. So we rewrite those to avoid it.

This should cover the lowest hanging fruits.

* Fix up ReadExactBytes - do ReadArrayHeaderBytes as well.

* Don't stop the timer. Makes benchmarks take forever. Simplify EndlessReader
  • Loading branch information
klauspost authored Sep 18, 2024
1 parent b78c5cd commit be78ef3
Show file tree
Hide file tree
Showing 8 changed files with 148 additions and 159 deletions.
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ module github.com/tinylib/msgp
go 1.20

require (
github.com/philhofer/fwd v1.1.3-0.20240612014219-fbbf4953d986
github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c
golang.org/x/tools v0.22.0
)

Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
github.com/philhofer/fwd v1.1.3-0.20240612014219-fbbf4953d986 h1:jYi87L8j62qkXzaYHAQAhEapgukhenIMZRBKTNRLHJ4=
github.com/philhofer/fwd v1.1.3-0.20240612014219-fbbf4953d986/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c h1:dAMKvw0MlJT1GshSTtih8C2gDs04w8dReiOGXrGLNoY=
github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0=
golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
Expand Down
14 changes: 8 additions & 6 deletions msgp/circular.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ type EndlessReader struct {

// NewEndlessReader returns a new endless reader
func NewEndlessReader(b []byte, tb timer) *EndlessReader {
// Double until we reach 4K.
for len(b) < 4<<10 {
b = append(b, b...)
}
return &EndlessReader{tb: tb, data: b, offset: 0}
}

Expand All @@ -24,16 +28,14 @@ func NewEndlessReader(b []byte, tb timer) *EndlessReader {
// fills the supplied slice while the benchmark
// timer is stopped.
func (c *EndlessReader) Read(p []byte) (int, error) {
c.tb.StopTimer()
var n int
l := len(p)
m := len(c.data)
nn := copy(p[n:], c.data[c.offset:])
n += nn
for n < l {
nn := copy(p[n:], c.data[c.offset:])
n += nn
c.offset += nn
c.offset %= m
n += copy(p[n:], c.data[:])
}
c.tb.StartTimer()
c.offset = (c.offset + l) % m
return n, nil
}
6 changes: 2 additions & 4 deletions msgp/json.go
Original file line number Diff line number Diff line change
Expand Up @@ -346,14 +346,12 @@ func rwExtension(dst jsWriter, src *Reader) (n int, err error) {
}

func rwString(dst jsWriter, src *Reader) (n int, err error) {
var p []byte
p, err = src.R.Peek(1)
lead, err := src.R.PeekByte()
if err != nil {
return
}
lead := p[0]
var read int

var p []byte
if isfixstr(lead) {
read = int(rfixstr(lead))
src.R.Skip(1)
Expand Down
69 changes: 28 additions & 41 deletions msgp/read.go
Original file line number Diff line number Diff line change
Expand Up @@ -236,13 +236,13 @@ func (m *Reader) BufferSize() int { return m.R.BufferSize() }

// NextType returns the next object type to be decoded.
func (m *Reader) NextType() (Type, error) {
p, err := m.R.Peek(1)
next, err := m.R.PeekByte()
if err != nil {
return InvalidType, err
}
t := getType(p[0])
t := getType(next)
if t == InvalidType {
return t, InvalidPrefixError(p[0])
return t, InvalidPrefixError(next)
}
if t == ExtensionType {
v, err := m.peekExtensionType()
Expand All @@ -264,8 +264,8 @@ func (m *Reader) NextType() (Type, error) {
// IsNil returns whether or not
// the next byte is a null messagepack byte
func (m *Reader) IsNil() bool {
p, err := m.R.Peek(1)
return err == nil && p[0] == mnil
p, err := m.R.PeekByte()
return err == nil && p == mnil
}

// getNextSize returns the size of the next object on the wire.
Expand All @@ -276,11 +276,10 @@ func (m *Reader) IsNil() bool {
// use uintptr b/c it's guaranteed to be large enough
// to hold whatever we can fit in memory.
func getNextSize(r *fwd.Reader) (uintptr, uintptr, error) {
b, err := r.Peek(1)
lead, err := r.PeekByte()
if err != nil {
return 0, 0, err
}
lead := b[0]
spec := getBytespec(lead)
size, mode := spec.size, spec.extra
if size == 0 {
Expand All @@ -289,7 +288,7 @@ func getNextSize(r *fwd.Reader) (uintptr, uintptr, error) {
if mode >= 0 {
return uintptr(size), uintptr(mode), nil
}
b, err = r.Peek(int(size))
b, err := r.Peek(int(size))
if err != nil {
return 0, 0, err
}
Expand Down Expand Up @@ -373,11 +372,10 @@ func (m *Reader) Skip() error {
func (m *Reader) ReadMapHeader() (sz uint32, err error) {
var p []byte
var lead byte
p, err = m.R.Peek(1)
lead, err = m.R.PeekByte()
if err != nil {
return
}
lead = p[0]
if isfixmap(lead) {
sz = uint32(rfixmap(lead))
_, err = m.R.Skip(1)
Expand Down Expand Up @@ -427,12 +425,12 @@ func (m *Reader) ReadMapKey(scratch []byte) ([]byte, error) {
// method; writing into the returned slice may
// corrupt future reads.
func (m *Reader) ReadMapKeyPtr() ([]byte, error) {
p, err := m.R.Peek(1)
lead, err := m.R.PeekByte()
if err != nil {
return nil, err
}
lead := p[0]
var read int
var p []byte
if isfixstr(lead) {
read = int(rfixstr(lead))
m.R.Skip(1)
Expand Down Expand Up @@ -471,18 +469,16 @@ fill:
// array header and returns the size of the array
// and the number of bytes read.
func (m *Reader) ReadArrayHeader() (sz uint32, err error) {
var lead byte
var p []byte
p, err = m.R.Peek(1)
lead, err := m.R.PeekByte()
if err != nil {
return
}
lead = p[0]
if isfixarray(lead) {
sz = uint32(rfixarray(lead))
_, err = m.R.Skip(1)
return
}
var p []byte
switch lead {
case marray16:
p, err = m.R.Next(3)
Expand All @@ -508,12 +504,12 @@ func (m *Reader) ReadArrayHeader() (sz uint32, err error) {

// ReadNil reads a 'nil' MessagePack byte from the reader
func (m *Reader) ReadNil() error {
p, err := m.R.Peek(1)
p, err := m.R.PeekByte()
if err != nil {
return err
}
if p[0] != mnil {
return badPrefix(NilType, p[0])
if p != mnil {
return badPrefix(NilType, p)
}
_, err = m.R.Skip(1)
return err
Expand Down Expand Up @@ -566,17 +562,17 @@ func (m *Reader) ReadFloat32() (f float32, err error) {

// ReadBool reads a bool from the reader
func (m *Reader) ReadBool() (b bool, err error) {
var p []byte
p, err = m.R.Peek(1)
var p byte
p, err = m.R.PeekByte()
if err != nil {
return
}
switch p[0] {
switch p {
case mtrue:
b = true
case mfalse:
default:
err = badPrefix(BoolType, p[0])
err = badPrefix(BoolType, p)
return
}
_, err = m.R.Skip(1)
Expand All @@ -592,12 +588,10 @@ func (m *Reader) ReadDuration() (d time.Duration, err error) {
// ReadInt64 reads an int64 from the reader
func (m *Reader) ReadInt64() (i int64, err error) {
var p []byte
var lead byte
p, err = m.R.Peek(1)
lead, err := m.R.PeekByte()
if err != nil {
return
}
lead = p[0]

if isfixint(lead) {
i = int64(rfixint(lead))
Expand Down Expand Up @@ -738,12 +732,10 @@ func (m *Reader) ReadInt() (i int, err error) {
// ReadUint64 reads a uint64 from the reader
func (m *Reader) ReadUint64() (u uint64, err error) {
var p []byte
var lead byte
p, err = m.R.Peek(1)
lead, err := m.R.PeekByte()
if err != nil {
return
}
lead = p[0]
if isfixint(lead) {
u = uint64(rfixint(lead))
_, err = m.R.Skip(1)
Expand Down Expand Up @@ -958,11 +950,11 @@ func (m *Reader) ReadBytes(scratch []byte) (b []byte, err error) {
// way.
func (m *Reader) ReadBytesHeader() (sz uint32, err error) {
var p []byte
p, err = m.R.Peek(1)
lead, err := m.R.PeekByte()
if err != nil {
return
}
switch p[0] {
switch lead {
case mbin8:
p, err = m.R.Next(2)
if err != nil {
Expand Down Expand Up @@ -1036,12 +1028,10 @@ func (m *Reader) ReadExactBytes(into []byte) error {
// if it is non-nil.
func (m *Reader) ReadStringAsBytes(scratch []byte) (b []byte, err error) {
var p []byte
var lead byte
p, err = m.R.Peek(1)
lead, err := m.R.PeekByte()
if err != nil {
return
}
lead = p[0]
var read int64

if isfixstr(lead) {
Expand Down Expand Up @@ -1088,17 +1078,16 @@ fill:
// for dealing with the next 'sz' bytes from
// the reader in an application-specific manner.
func (m *Reader) ReadStringHeader() (sz uint32, err error) {
var p []byte
p, err = m.R.Peek(1)
lead, err := m.R.PeekByte()
if err != nil {
return
}
lead := p[0]
if isfixstr(lead) {
sz = uint32(rfixstr(lead))
m.R.Skip(1)
return
}
var p []byte
switch lead {
case mstr8:
p, err = m.R.Next(2)
Expand Down Expand Up @@ -1129,15 +1118,13 @@ func (m *Reader) ReadStringHeader() (sz uint32, err error) {

// ReadString reads a utf-8 string from the reader
func (m *Reader) ReadString() (s string, err error) {
var p []byte
var lead byte
var read int64
p, err = m.R.Peek(1)
lead, err := m.R.PeekByte()
if err != nil {
return
}
lead = p[0]

var p []byte
if isfixstr(lead) {
read = int64(rfixstr(lead))
m.R.Skip(1)
Expand Down
Loading

0 comments on commit be78ef3

Please sign in to comment.