diff --git a/CHANGELOG.md b/CHANGELOG.md index 0339a06..266e292 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,14 @@ -## v6 (Basekick-Labs fork) +## v6.1 (unreleased) + +### Performance + +- **decode:** `readCode` bsr fast path — when decoding from a byte slice, reads directly from the underlying array instead of dispatching through the `io.ByteReader` interface; eliminates ~900M interface calls/sec at Arc's throughput ([#57](https://github.com/Basekick-Labs/msgpack/issues/57)) (StructUnmarshal **-7.5%**, StructUnmarshalPartially **-6.1%**) +- **decode:** `PeekCode` bsr fast path — peeks directly at `bsr.data[bsr.pos]` instead of `ReadByte` + `UnreadByte` (two interface calls) ([#59](https://github.com/Basekick-Labs/msgpack/issues/59)) +- **encode:** pool `OmitEmpty` filtered field slices via `sync.Pool` — when fields are actually omitted, the allocated `[]*field` slice is now returned to a pool for reuse instead of being GC'd ([#58](https://github.com/Basekick-Labs/msgpack/issues/58)) + +--- + +## v6.0.0 (Basekick-Labs fork) ### Performance diff --git a/decode.go b/decode.go index d412e48..7896774 100644 --- a/decode.go +++ b/decode.go @@ -610,6 +610,14 @@ func (d *Decoder) DecodeRaw() (RawMessage, error) { // PeekCode returns the next MessagePack code without advancing the reader. // Subpackage msgpack/codes defines the list of available msgpcode. func (d *Decoder) PeekCode() (byte, error) { + // Fast path: when decoding from a byte slice, peek directly + // to avoid two interface method calls (ReadByte + UnreadByte). + if d.s == &d.bsr { + if d.bsr.pos >= len(d.bsr.data) { + return 0, io.EOF + } + return d.bsr.data[d.bsr.pos], nil + } c, err := d.s.ReadByte() if err != nil { return 0, err @@ -634,6 +642,19 @@ func (d *Decoder) hasNilCode() bool { } func (d *Decoder) readCode() (byte, error) { + // Fast path: when decoding from a byte slice, read directly + // to avoid interface method dispatch on the hottest decode function. + if d.s == &d.bsr { + if d.bsr.pos >= len(d.bsr.data) { + return 0, io.EOF + } + c := d.bsr.data[d.bsr.pos] + d.bsr.pos++ + if d.rec != nil { + d.rec = append(d.rec, c) + } + return c, nil + } c, err := d.s.ReadByte() if err != nil { return 0, err diff --git a/encode_map.go b/encode_map.go index 563f9cf..b8e5a38 100644 --- a/encode_map.go +++ b/encode_map.go @@ -279,21 +279,40 @@ func encodeStructValue(e *Encoder, strct reflect.Value) error { fields := structFields.OmitEmpty(e, strct) if err := e.EncodeMapLen(len(fields)); err != nil { + putFilteredFields(structFields, fields) return err } for _, f := range fields { if err := e.EncodeString(f.name); err != nil { + putFilteredFields(structFields, fields) return err } if err := f.EncodeValue(e, strct); err != nil { + putFilteredFields(structFields, fields) return err } } + putFilteredFields(structFields, fields) return nil } +// putFilteredFields returns a pooled filtered field slice. +// It is a no-op when the slice is the original fs.List (not pooled). +func putFilteredFields(fs *fields, filtered []*field) { + if len(filtered) > 0 && len(fs.List) > 0 && &filtered[0] == &fs.List[0] { + return + } + for i := range filtered { + filtered[i] = nil + } + if cap(filtered) <= 64 { + s := filtered + filteredFieldsPool.Put(&s) + } +} + func encodeStructValueAsArray(e *Encoder, strct reflect.Value, fields []*field) error { if err := e.EncodeArrayLen(len(fields)); err != nil { return err diff --git a/types.go b/types.go index 9e073f6..1a397be 100644 --- a/types.go +++ b/types.go @@ -120,6 +120,13 @@ func (f *field) DecodeValue(d *Decoder, strct reflect.Value) error { return f.decoder(d, v) } +var filteredFieldsPool = sync.Pool{ + New: func() interface{} { + s := make([]*field, 0, 16) + return &s + }, +} + //------------------------------------------------------------------------------ type fields struct { @@ -172,14 +179,18 @@ func (fs *fields) OmitEmpty(e *Encoder, strct reflect.Value) []*field { return fs.List } - // Second pass: build the filtered slice only when necessary. - fields := make([]*field, 0, n) + // Second pass: build the filtered slice from pool. + sp := filteredFieldsPool.Get().(*[]*field) + fields := (*sp)[:0] + if cap(fields) < n { + fields = make([]*field, 0, n) + } for _, f := range fs.List { if !f.Omit(e, strct) { fields = append(fields, f) } } - + *sp = fields return fields }