Skip to content

Commit 4027706

Browse files
committed
perf: bsr fast paths for readCode/PeekCode + pool OmitEmpty field slices
Three optimizations targeting decode throughput and encode GC pressure: 1. readCode() bsr fast path (#57): read directly from byte-slice data instead of going through interface dispatch on the hottest decode function. Handles d.rec recording mode inline. 2. PeekCode() bsr fast path (#59): peek directly at byte-slice data avoiding ReadByte+UnreadByte interface dispatch. 3. Pool OmitEmpty filtered field slices (#58): use sync.Pool for the []*field slice allocated when struct fields are omitted, returned via putFilteredFields after iteration in encodeStructValue. Benchmark results (Apple M3 Max, 3 iterations): StructUnmarshal: 361 ns → 334 ns (-7.5%) StructUnmarshalPartially: 247 ns → 232 ns (-6.1%) MapIntInt: 328 ns → 314 ns (-4.3%) StructManual: 569 ns → 544 ns (-4.4%) Closes #57, closes #59, closes #58
1 parent c0a3add commit 4027706

3 files changed

Lines changed: 54 additions & 3 deletions

File tree

decode.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -610,6 +610,14 @@ func (d *Decoder) DecodeRaw() (RawMessage, error) {
610610
// PeekCode returns the next MessagePack code without advancing the reader.
611611
// Subpackage msgpack/codes defines the list of available msgpcode.
612612
func (d *Decoder) PeekCode() (byte, error) {
613+
// Fast path: when decoding from a byte slice, peek directly
614+
// to avoid two interface method calls (ReadByte + UnreadByte).
615+
if d.s == &d.bsr {
616+
if d.bsr.pos >= len(d.bsr.data) {
617+
return 0, io.EOF
618+
}
619+
return d.bsr.data[d.bsr.pos], nil
620+
}
613621
c, err := d.s.ReadByte()
614622
if err != nil {
615623
return 0, err
@@ -634,6 +642,19 @@ func (d *Decoder) hasNilCode() bool {
634642
}
635643

636644
func (d *Decoder) readCode() (byte, error) {
645+
// Fast path: when decoding from a byte slice, read directly
646+
// to avoid interface method dispatch on the hottest decode function.
647+
if d.s == &d.bsr {
648+
if d.bsr.pos >= len(d.bsr.data) {
649+
return 0, io.EOF
650+
}
651+
c := d.bsr.data[d.bsr.pos]
652+
d.bsr.pos++
653+
if d.rec != nil {
654+
d.rec = append(d.rec, c)
655+
}
656+
return c, nil
657+
}
637658
c, err := d.s.ReadByte()
638659
if err != nil {
639660
return 0, err

encode_map.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,21 +279,40 @@ func encodeStructValue(e *Encoder, strct reflect.Value) error {
279279
fields := structFields.OmitEmpty(e, strct)
280280

281281
if err := e.EncodeMapLen(len(fields)); err != nil {
282+
putFilteredFields(structFields, fields)
282283
return err
283284
}
284285

285286
for _, f := range fields {
286287
if err := e.EncodeString(f.name); err != nil {
288+
putFilteredFields(structFields, fields)
287289
return err
288290
}
289291
if err := f.EncodeValue(e, strct); err != nil {
292+
putFilteredFields(structFields, fields)
290293
return err
291294
}
292295
}
293296

297+
putFilteredFields(structFields, fields)
294298
return nil
295299
}
296300

301+
// putFilteredFields returns a pooled filtered field slice.
302+
// It is a no-op when the slice is the original fs.List (not pooled).
303+
func putFilteredFields(fs *fields, filtered []*field) {
304+
if len(filtered) > 0 && len(fs.List) > 0 && &filtered[0] == &fs.List[0] {
305+
return
306+
}
307+
for i := range filtered {
308+
filtered[i] = nil
309+
}
310+
if cap(filtered) <= 64 {
311+
s := filtered
312+
filteredFieldsPool.Put(&s)
313+
}
314+
}
315+
297316
func encodeStructValueAsArray(e *Encoder, strct reflect.Value, fields []*field) error {
298317
if err := e.EncodeArrayLen(len(fields)); err != nil {
299318
return err

types.go

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,13 @@ func (f *field) DecodeValue(d *Decoder, strct reflect.Value) error {
120120
return f.decoder(d, v)
121121
}
122122

123+
var filteredFieldsPool = sync.Pool{
124+
New: func() interface{} {
125+
s := make([]*field, 0, 16)
126+
return &s
127+
},
128+
}
129+
123130
//------------------------------------------------------------------------------
124131

125132
type fields struct {
@@ -172,14 +179,18 @@ func (fs *fields) OmitEmpty(e *Encoder, strct reflect.Value) []*field {
172179
return fs.List
173180
}
174181

175-
// Second pass: build the filtered slice only when necessary.
176-
fields := make([]*field, 0, n)
182+
// Second pass: build the filtered slice from pool.
183+
sp := filteredFieldsPool.Get().(*[]*field)
184+
fields := (*sp)[:0]
185+
if cap(fields) < n {
186+
fields = make([]*field, 0, n)
187+
}
177188
for _, f := range fs.List {
178189
if !f.Omit(e, strct) {
179190
fields = append(fields, f)
180191
}
181192
}
182-
193+
*sp = fields
183194
return fields
184195
}
185196

0 commit comments

Comments
 (0)