Skip to content

Commit 4b0fe59

Browse files
committed
bsoncodec/bsonrw: eliminate encoding allocations
This commit eliminates all allocations during marshalling (except for the final []byte slice allocation, which is unavoidable). This matches the behavior of encoding/json. It also prevents the vwPool from leaking writers and includes a few small optimizations to the value_writer. Other Changes: * bsoncodec: reduce use of reflect.Value.Interface() * bson: use a buffer pool instead of SliceWriter to eliminate 2 allocs * bsonrw: use references to slice indexes This work builds off of #1313 and uses the BenchmarkCode* suite added by that PR. The combined performance improvement is below: ``` goos: darwin goarch: arm64 pkg: go.mongodb.org/mongo-driver/bson │ base.20.txt │ new.20.txt │ │ sec/op │ sec/op vs base │ CodeUnmarshal/BSON-10 3.177m ± 1% 2.202m ± 1% -30.68% (p=0.000 n=20) CodeMarshal/BSON-10 2922.7µ ± 0% 757.1µ ± 2% -74.10% (p=0.000 n=20) geomean 3.047m 1.291m -57.63% │ base.20.txt │ new.20.txt │ │ B/s │ B/s vs base │ CodeUnmarshal/BSON-10 582.5Mi ± 1% 840.4Mi ± 1% +44.26% (p=0.000 n=20) CodeMarshal/BSON-10 633.2Mi ± 0% 2444.3Mi ± 2% +286.03% (p=0.000 n=20) geomean 607.3Mi 1.400Gi +135.99% │ base.20.txt │ new.20.txt │ │ B/op │ B/op vs base │ CodeUnmarshal/BSON-10 4.219Mi ± 0% 4.148Mi ± 0% -1.69% (p=0.000 n=20) CodeMarshal/BSON-10 2.818Mi ± 3% 1.630Mi ± 0% -42.16% (p=0.000 n=20) geomean 3.448Mi 2.600Mi -24.59% │ base.20.txt │ new.20.txt │ │ allocs/op │ allocs/op vs base │ CodeUnmarshal/BSON-10 230.4k ± 0% 220.7k ± 0% -4.21% (p=0.000 n=20) CodeMarshal/BSON-10 94066.000 ± 0% 1.000 ± 0% -100.00% (p=0.000 n=20) geomean 147.2k 469.7 -99.68% ```
1 parent db543ec commit 4b0fe59

File tree

8 files changed

+92
-83
lines changed

8 files changed

+92
-83
lines changed

bson/bsoncodec/slice_codec.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ func (sc SliceCodec) EncodeValue(ec EncodeContext, vw bsonrw.ValueWriter, val re
6262
}
6363

6464
// If we have a []primitive.E we want to treat it as a document instead of as an array.
65-
if val.Type().ConvertibleTo(tD) {
65+
if val.Type() == tD || val.Type().ConvertibleTo(tD) {
6666
d := val.Convert(tD).Interface().(primitive.D)
6767

6868
dw, err := vw.WriteDocument()

bson/bsoncodec/struct_codec.go

+24-49
Original file line numberDiff line numberDiff line change
@@ -192,15 +192,14 @@ func (sc *StructCodec) EncodeValue(ec EncodeContext, vw bsonrw.ValueWriter, val
192192
encoder := desc.encoder
193193

194194
var zero bool
195-
rvInterface := rv.Interface()
196195
if cz, ok := encoder.(CodecZeroer); ok {
197-
zero = cz.IsTypeZero(rvInterface)
196+
zero = cz.IsTypeZero(rv.Interface())
198197
} else if rv.Kind() == reflect.Interface {
199198
// isZero will not treat an interface rv as an interface, so we need to check for the
200199
// zero interface separately.
201200
zero = rv.IsNil()
202201
} else {
203-
zero = isZero(rvInterface, sc.EncodeOmitDefaultStruct || ec.omitZeroStruct)
202+
zero = isZero(rv, sc.EncodeOmitDefaultStruct || ec.omitZeroStruct)
204203
}
205204
if desc.omitEmpty && zero {
206205
continue
@@ -394,56 +393,32 @@ func (sc *StructCodec) DecodeValue(dc DecodeContext, vr bsonrw.ValueReader, val
394393
return nil
395394
}
396395

397-
func isZero(i interface{}, omitZeroStruct bool) bool {
398-
v := reflect.ValueOf(i)
399-
400-
// check the value validity
401-
if !v.IsValid() {
402-
return true
396+
func isZero(v reflect.Value, omitZeroStruct bool) bool {
397+
kind := v.Kind()
398+
if (kind != reflect.Pointer || !v.IsNil()) && v.Type().Implements(tZeroer) {
399+
return v.Interface().(Zeroer).IsZero()
403400
}
404-
405-
if z, ok := v.Interface().(Zeroer); ok && (v.Kind() != reflect.Ptr || !v.IsNil()) {
406-
return z.IsZero()
407-
}
408-
409-
switch v.Kind() {
410-
case reflect.Array, reflect.Map, reflect.Slice, reflect.String:
411-
return v.Len() == 0
412-
case reflect.Bool:
413-
return !v.Bool()
414-
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
415-
return v.Int() == 0
416-
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
417-
return v.Uint() == 0
418-
case reflect.Float32, reflect.Float64:
419-
return v.Float() == 0
420-
case reflect.Interface, reflect.Ptr:
421-
return v.IsNil()
422-
case reflect.Struct:
401+
if kind == reflect.Struct {
423402
if !omitZeroStruct {
424403
return false
425404
}
426-
427-
// TODO(GODRIVER-2820): Update the logic to be able to handle private struct fields.
428-
// TODO Use condition "reflect.Zero(v.Type()).Equal(v)" instead.
429-
430405
vt := v.Type()
431406
if vt == tTime {
432407
return v.Interface().(time.Time).IsZero()
433408
}
434-
for i := 0; i < v.NumField(); i++ {
435-
if vt.Field(i).PkgPath != "" && !vt.Field(i).Anonymous {
409+
numField := vt.NumField()
410+
for i := 0; i < numField; i++ {
411+
ff := vt.Field(i)
412+
if ff.PkgPath != "" && !ff.Anonymous {
436413
continue // Private field
437414
}
438-
fld := v.Field(i)
439-
if !isZero(fld.Interface(), omitZeroStruct) {
415+
if !isZero(v.Field(i), omitZeroStruct) {
440416
return false
441417
}
442418
}
443419
return true
444420
}
445-
446-
return false
421+
return !v.IsValid() || v.IsZero()
447422
}
448423

449424
type structDescription struct {
@@ -700,21 +675,21 @@ func getInlineField(val reflect.Value, index []int) (reflect.Value, error) {
700675

701676
// DeepZero returns recursive zero object
702677
func deepZero(st reflect.Type) (result reflect.Value) {
703-
result = reflect.Indirect(reflect.New(st))
704-
705-
if result.Kind() == reflect.Struct {
706-
for i := 0; i < result.NumField(); i++ {
707-
if f := result.Field(i); f.Kind() == reflect.Ptr {
708-
if f.CanInterface() {
709-
if ft := reflect.TypeOf(f.Interface()); ft.Elem().Kind() == reflect.Struct {
710-
result.Field(i).Set(recursivePointerTo(deepZero(ft.Elem())))
711-
}
678+
if st.Kind() == reflect.Struct {
679+
numField := st.NumField()
680+
for i := 0; i < numField; i++ {
681+
if result == emptyValue {
682+
result = reflect.Indirect(reflect.New(st))
683+
}
684+
f := result.Field(i)
685+
if f.CanInterface() {
686+
if f.Type().Kind() == reflect.Struct {
687+
result.Field(i).Set(recursivePointerTo(deepZero(f.Type().Elem())))
712688
}
713689
}
714690
}
715691
}
716-
717-
return
692+
return result
718693
}
719694

720695
// recursivePointerTo calls reflect.New(v.Type) but recursively for its fields inside

bson/bsoncodec/struct_codec_test.go

+2-1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
package bsoncodec
88

99
import (
10+
"reflect"
1011
"testing"
1112
"time"
1213

@@ -147,7 +148,7 @@ func TestIsZero(t *testing.T) {
147148
t.Run(tc.description, func(t *testing.T) {
148149
t.Parallel()
149150

150-
got := isZero(tc.value, tc.omitZeroStruct)
151+
got := isZero(reflect.ValueOf(tc.value), tc.omitZeroStruct)
151152
assert.Equal(t, tc.want, got, "expected and actual isZero return are different")
152153
})
153154
}

bson/bsoncodec/types.go

+1
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ var tValueUnmarshaler = reflect.TypeOf((*ValueUnmarshaler)(nil)).Elem()
3434
var tMarshaler = reflect.TypeOf((*Marshaler)(nil)).Elem()
3535
var tUnmarshaler = reflect.TypeOf((*Unmarshaler)(nil)).Elem()
3636
var tProxy = reflect.TypeOf((*Proxy)(nil)).Elem()
37+
var tZeroer = reflect.TypeOf((*Zeroer)(nil)).Elem()
3738

3839
var tBinary = reflect.TypeOf(primitive.Binary{})
3940
var tUndefined = reflect.TypeOf(primitive.Undefined{})

bson/bsonrw/copier.go

+3-3
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,7 @@ func (c Copier) AppendDocumentBytes(dst []byte, src ValueReader) ([]byte, error)
193193
}
194194

195195
vw := vwPool.Get().(*valueWriter)
196-
defer vwPool.Put(vw)
196+
defer putValueWriter(vw)
197197

198198
vw.reset(dst)
199199

@@ -213,7 +213,7 @@ func (c Copier) AppendArrayBytes(dst []byte, src ValueReader) ([]byte, error) {
213213
}
214214

215215
vw := vwPool.Get().(*valueWriter)
216-
defer vwPool.Put(vw)
216+
defer putValueWriter(vw)
217217

218218
vw.reset(dst)
219219

@@ -258,7 +258,7 @@ func (c Copier) AppendValueBytes(dst []byte, src ValueReader) (bsontype.Type, []
258258
}
259259

260260
vw := vwPool.Get().(*valueWriter)
261-
defer vwPool.Put(vw)
261+
defer putValueWriter(vw)
262262

263263
start := len(dst)
264264

bson/bsonrw/value_reader.go

+10-2
Original file line numberDiff line numberDiff line change
@@ -739,8 +739,7 @@ func (vr *valueReader) ReadValue() (ValueReader, error) {
739739
return nil, ErrEOA
740740
}
741741

742-
_, err = vr.readCString()
743-
if err != nil {
742+
if err := vr.consumeCString(); err != nil {
744743
return nil, err
745744
}
746745

@@ -794,6 +793,15 @@ func (vr *valueReader) readByte() (byte, error) {
794793
return vr.d[vr.offset-1], nil
795794
}
796795

796+
func (vr *valueReader) consumeCString() error {
797+
idx := bytes.IndexByte(vr.d[vr.offset:], 0x00)
798+
if idx < 0 {
799+
return io.EOF
800+
}
801+
vr.offset += int64(idx) + 1
802+
return nil
803+
}
804+
797805
func (vr *valueReader) readCString() (string, error) {
798806
idx := bytes.IndexByte(vr.d[vr.offset:], 0x00)
799807
if idx < 0 {

bson/bsonrw/value_writer.go

+39-24
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,13 @@ var vwPool = sync.Pool{
2828
},
2929
}
3030

31+
func putValueWriter(vw *valueWriter) {
32+
if vw != nil {
33+
vw.w = nil // don't leak the writer
34+
vwPool.Put(vw)
35+
}
36+
}
37+
3138
// BSONValueWriterPool is a pool for BSON ValueWriters.
3239
//
3340
// Deprecated: BSONValueWriterPool will not be supported in Go Driver 2.0.
@@ -149,32 +156,21 @@ type valueWriter struct {
149156
}
150157

151158
func (vw *valueWriter) advanceFrame() {
152-
if vw.frame+1 >= int64(len(vw.stack)) { // We need to grow the stack
153-
length := len(vw.stack)
154-
if length+1 >= cap(vw.stack) {
155-
// double it
156-
buf := make([]vwState, 2*cap(vw.stack)+1)
157-
copy(buf, vw.stack)
158-
vw.stack = buf
159-
}
160-
vw.stack = vw.stack[:length+1]
161-
}
162159
vw.frame++
160+
if vw.frame >= int64(len(vw.stack)) {
161+
vw.stack = append(vw.stack, vwState{})
162+
}
163163
}
164164

165165
func (vw *valueWriter) push(m mode) {
166166
vw.advanceFrame()
167167

168168
// Clean the stack
169-
vw.stack[vw.frame].mode = m
170-
vw.stack[vw.frame].key = ""
171-
vw.stack[vw.frame].arrkey = 0
172-
vw.stack[vw.frame].start = 0
169+
vw.stack[vw.frame] = vwState{mode: m}
173170

174-
vw.stack[vw.frame].mode = m
175171
switch m {
176172
case mDocument, mArray, mCodeWithScope:
177-
vw.reserveLength()
173+
vw.reserveLength() // WARN: this is not needed
178174
}
179175
}
180176

@@ -213,6 +209,7 @@ func newValueWriter(w io.Writer) *valueWriter {
213209
return vw
214210
}
215211

212+
// TODO: only used in tests
216213
func newValueWriterFromSlice(buf []byte) *valueWriter {
217214
vw := new(valueWriter)
218215
stack := make([]vwState, 1, 5)
@@ -249,17 +246,17 @@ func (vw *valueWriter) invalidTransitionError(destination mode, name string, mod
249246
}
250247

251248
func (vw *valueWriter) writeElementHeader(t bsontype.Type, destination mode, callerName string, addmodes ...mode) error {
252-
switch vw.stack[vw.frame].mode {
249+
frame := &vw.stack[vw.frame]
250+
switch frame.mode {
253251
case mElement:
254-
key := vw.stack[vw.frame].key
252+
key := frame.key
255253
if !isValidCString(key) {
256254
return errors.New("BSON element key cannot contain null bytes")
257255
}
258-
259-
vw.buf = bsoncore.AppendHeader(vw.buf, t, key)
256+
vw.appendHeader(t, key)
260257
case mValue:
261258
// TODO: Do this with a cache of the first 1000 or so array keys.
262-
vw.buf = bsoncore.AppendHeader(vw.buf, t, strconv.Itoa(vw.stack[vw.frame].arrkey))
259+
vw.appendIntHeader(t, frame.arrkey)
263260
default:
264261
modes := []mode{mElement, mValue}
265262
if addmodes != nil {
@@ -601,9 +598,11 @@ func (vw *valueWriter) writeLength() error {
601598
if length > maxSize {
602599
return errMaxDocumentSizeExceeded{size: int64(len(vw.buf))}
603600
}
604-
length = length - int(vw.stack[vw.frame].start)
605-
start := vw.stack[vw.frame].start
601+
frame := &vw.stack[vw.frame]
602+
length = length - int(frame.start)
603+
start := frame.start
606604

605+
_ = vw.buf[start+3] // BCE
607606
vw.buf[start+0] = byte(length)
608607
vw.buf[start+1] = byte(length >> 8)
609608
vw.buf[start+2] = byte(length >> 16)
@@ -612,5 +611,21 @@ func (vw *valueWriter) writeLength() error {
612611
}
613612

614613
func isValidCString(cs string) bool {
615-
return !strings.ContainsRune(cs, '\x00')
614+
return strings.IndexByte(cs, 0) == -1
615+
}
616+
617+
// appendHeader is the same as bsoncore.AppendHeader but does not check if the
618+
// key is a valid C string since the caller has already checked for that.
619+
//
620+
// The caller of this function must check if key is a valid C string.
621+
func (vw *valueWriter) appendHeader(t bsontype.Type, key string) {
622+
vw.buf = bsoncore.AppendType(vw.buf, t)
623+
vw.buf = append(vw.buf, key...)
624+
vw.buf = append(vw.buf, 0x00)
625+
}
626+
627+
func (vw *valueWriter) appendIntHeader(t bsontype.Type, key int) {
628+
vw.buf = bsoncore.AppendType(vw.buf, t)
629+
vw.buf = strconv.AppendInt(vw.buf, int64(key), 10)
630+
vw.buf = append(vw.buf, 0x00)
616631
}

bson/marshal.go

+12-3
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ package bson
99
import (
1010
"bytes"
1111
"encoding/json"
12+
"sync"
1213

1314
"go.mongodb.org/mongo-driver/bson/bsoncodec"
1415
"go.mongodb.org/mongo-driver/bson/bsonrw"
@@ -141,6 +142,13 @@ func MarshalAppendWithRegistry(r *bsoncodec.Registry, dst []byte, val interface{
141142
return MarshalAppendWithContext(bsoncodec.EncodeContext{Registry: r}, dst, val)
142143
}
143144

145+
// Pool of buffers for marshalling BSON.
146+
var bufPool = sync.Pool{
147+
New: func() interface{} {
148+
return new(bytes.Buffer)
149+
},
150+
}
151+
144152
// MarshalAppendWithContext will encode val as a BSON document using Registry r and EncodeContext ec and append the
145153
// bytes to dst. If dst is not large enough to hold the bytes, it will be grown. If val is not a type that can be
146154
// transformed into a document, MarshalValueAppendWithContext should be used instead.
@@ -162,8 +170,9 @@ func MarshalAppendWithRegistry(r *bsoncodec.Registry, dst []byte, val interface{
162170
//
163171
// See [Encoder] for more examples.
164172
func MarshalAppendWithContext(ec bsoncodec.EncodeContext, dst []byte, val interface{}) ([]byte, error) {
165-
sw := new(bsonrw.SliceWriter)
166-
*sw = dst
173+
sw := bufPool.Get().(*bytes.Buffer)
174+
defer bufPool.Put(sw)
175+
sw.Reset()
167176
vw := bvwPool.Get(sw)
168177
defer bvwPool.Put(vw)
169178

@@ -184,7 +193,7 @@ func MarshalAppendWithContext(ec bsoncodec.EncodeContext, dst []byte, val interf
184193
return nil, err
185194
}
186195

187-
return *sw, nil
196+
return append(dst, sw.Bytes()...), nil
188197
}
189198

190199
// MarshalValue returns the BSON encoding of val.

0 commit comments

Comments
 (0)