Skip to content

Commit 97b2de0

Browse files
committed
bson: improve marshal/unmarshal performance by ~58% and ~29%
This commit improves BSON marshaling performance by ~58% and unmarshaling performance by ~29% by replacing the mutex based decoder/encoder caches with sync.Map, which can often avoid locking and is ideally suited for caches that only grow. The commit also adds the BenchmarkCodeMarshal and BenchmarkCodeUnmarshal benchmarks from the Go standard library's encoding/json package since they do an excellent job of stress testing parallel encoding/decoding (a common use case in a database driver) and are how the lock contention that led to this commit were discovered. ``` 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.192m ± 1% 2.246m ± 1% -29.64% (p=0.000 n=20) CodeUnmarshal/JSON-10 2.735m ± 1% 2.737m ± 0% ~ (p=0.640 n=20) CodeMarshal/BSON-10 2.972m ± 0% 1.221m ± 3% -58.93% (p=0.000 n=20) CodeMarshal/JSON-10 471.0µ ± 1% 464.6µ ± 0% -1.36% (p=0.000 n=20) geomean 1.870m 1.366m -26.92% │ base.20.txt │ new.20.txt │ │ B/s │ B/s vs base │ CodeUnmarshal/BSON-10 579.7Mi ± 1% 823.9Mi ± 1% +42.13% (p=0.000 n=20) CodeUnmarshal/JSON-10 676.6Mi ± 1% 676.2Mi ± 0% ~ (p=0.640 n=20) CodeMarshal/BSON-10 622.7Mi ± 0% 1516.2Mi ± 3% +143.46% (p=0.000 n=20) CodeMarshal/JSON-10 3.837Gi ± 1% 3.890Gi ± 0% +1.38% (p=0.000 n=20) geomean 989.8Mi 1.323Gi +36.84% │ base.20.txt │ new.20.txt │ │ B/op │ B/op vs base │ CodeUnmarshal/BSON-10 4.219Mi ± 0% 4.219Mi ± 0% ~ (p=0.077 n=20) CodeUnmarshal/JSON-10 2.904Mi ± 0% 2.904Mi ± 0% ~ (p=0.672 n=20) CodeMarshal/BSON-10 2.821Mi ± 1% 2.776Mi ± 2% -1.59% (p=0.023 n=20) CodeMarshal/JSON-10 1.857Mi ± 0% 1.859Mi ± 0% ~ (p=0.331 n=20) geomean 2.830Mi 2.820Mi -0.37% │ base.20.txt │ new.20.txt │ │ allocs/op │ allocs/op vs base │ CodeUnmarshal/BSON-10 230.4k ± 0% 230.4k ± 0% ~ (p=1.000 n=20) CodeUnmarshal/JSON-10 92.67k ± 0% 92.67k ± 0% ~ (p=1.000 n=20) ¹ CodeMarshal/BSON-10 94.07k ± 0% 94.07k ± 0% ~ (p=0.112 n=20) CodeMarshal/JSON-10 1.000 ± 0% 1.000 ± 0% ~ (p=1.000 n=20) ¹ geomean 6.694k 6.694k +0.00% ¹ all samples are equal ```
1 parent 8489898 commit 97b2de0

File tree

8 files changed

+639
-191
lines changed

8 files changed

+639
-191
lines changed

bson/benchmark_test.go

+142
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,15 @@
77
package bson
88

99
import (
10+
"bytes"
1011
"compress/gzip"
1112
"encoding/json"
1213
"fmt"
14+
"io"
1315
"io/ioutil"
1416
"os"
1517
"path"
18+
"sync"
1619
"testing"
1720
)
1821

@@ -129,10 +132,20 @@ var nestedInstance = nestedtest1{
129132

130133
const extendedBSONDir = "../testdata/extended_bson"
131134

135+
var (
136+
extJSONFiles map[string]map[string]interface{}
137+
extJSONFilesMu sync.Mutex
138+
)
139+
132140
// readExtJSONFile reads the GZIP-compressed extended JSON document from the given filename in the
133141
// "extended BSON" test data directory (../testdata/extended_bson) and returns it as a
134142
// map[string]interface{}. It panics on any errors.
135143
func readExtJSONFile(filename string) map[string]interface{} {
144+
extJSONFilesMu.Lock()
145+
defer extJSONFilesMu.Unlock()
146+
if v, ok := extJSONFiles[filename]; ok {
147+
return v
148+
}
136149
filePath := path.Join(extendedBSONDir, filename)
137150
file, err := os.Open(filePath)
138151
if err != nil {
@@ -161,6 +174,10 @@ func readExtJSONFile(filename string) map[string]interface{} {
161174
panic(fmt.Sprintf("error unmarshalling extended JSON: %s", err))
162175
}
163176

177+
if extJSONFiles == nil {
178+
extJSONFiles = make(map[string]map[string]interface{})
179+
}
180+
extJSONFiles[filename] = v
164181
return v
165182
}
166183

@@ -305,3 +322,128 @@ func BenchmarkUnmarshal(b *testing.B) {
305322
})
306323
}
307324
}
325+
326+
// The following benchmarks are copied from the Go standard library's
327+
// encoding/json package.
328+
329+
type codeResponse struct {
330+
Tree *codeNode `json:"tree"`
331+
Username string `json:"username"`
332+
}
333+
334+
type codeNode struct {
335+
Name string `json:"name"`
336+
Kids []*codeNode `json:"kids"`
337+
CLWeight float64 `json:"cl_weight"`
338+
Touches int `json:"touches"`
339+
MinT int64 `json:"min_t"`
340+
MaxT int64 `json:"max_t"`
341+
MeanT int64 `json:"mean_t"`
342+
}
343+
344+
var codeJSON []byte
345+
var codeBSON []byte
346+
var codeStruct codeResponse
347+
348+
func codeInit() {
349+
f, err := os.Open("testdata/code.json.gz")
350+
if err != nil {
351+
panic(err)
352+
}
353+
defer f.Close()
354+
gz, err := gzip.NewReader(f)
355+
if err != nil {
356+
panic(err)
357+
}
358+
data, err := io.ReadAll(gz)
359+
if err != nil {
360+
panic(err)
361+
}
362+
363+
codeJSON = data
364+
365+
if err := json.Unmarshal(codeJSON, &codeStruct); err != nil {
366+
panic("json.Unmarshal code.json: " + err.Error())
367+
}
368+
369+
if data, err = json.Marshal(&codeStruct); err != nil {
370+
panic("json.Marshal code.json: " + err.Error())
371+
}
372+
373+
if codeBSON, err = Marshal(&codeStruct); err != nil {
374+
panic("Marshal code.json: " + err.Error())
375+
}
376+
377+
if !bytes.Equal(data, codeJSON) {
378+
println("different lengths", len(data), len(codeJSON))
379+
for i := 0; i < len(data) && i < len(codeJSON); i++ {
380+
if data[i] != codeJSON[i] {
381+
println("re-marshal: changed at byte", i)
382+
println("orig: ", string(codeJSON[i-10:i+10]))
383+
println("new: ", string(data[i-10:i+10]))
384+
break
385+
}
386+
}
387+
panic("re-marshal code.json: different result")
388+
}
389+
}
390+
391+
func BenchmarkCodeUnmarshal(b *testing.B) {
392+
b.ReportAllocs()
393+
if codeJSON == nil {
394+
b.StopTimer()
395+
codeInit()
396+
b.StartTimer()
397+
}
398+
b.Run("BSON", func(b *testing.B) {
399+
b.RunParallel(func(pb *testing.PB) {
400+
for pb.Next() {
401+
var r codeResponse
402+
if err := Unmarshal(codeBSON, &r); err != nil {
403+
b.Fatal("Unmarshal:", err)
404+
}
405+
}
406+
})
407+
b.SetBytes(int64(len(codeJSON)))
408+
})
409+
b.Run("JSON", func(b *testing.B) {
410+
b.RunParallel(func(pb *testing.PB) {
411+
for pb.Next() {
412+
var r codeResponse
413+
if err := json.Unmarshal(codeJSON, &r); err != nil {
414+
b.Fatal("json.Unmarshal:", err)
415+
}
416+
}
417+
})
418+
b.SetBytes(int64(len(codeJSON)))
419+
})
420+
}
421+
422+
func BenchmarkCodeMarshal(b *testing.B) {
423+
b.ReportAllocs()
424+
if codeJSON == nil {
425+
b.StopTimer()
426+
codeInit()
427+
b.StartTimer()
428+
}
429+
b.Run("BSON", func(b *testing.B) {
430+
b.RunParallel(func(pb *testing.PB) {
431+
for pb.Next() {
432+
if _, err := Marshal(&codeStruct); err != nil {
433+
b.Fatal("Marshal:", err)
434+
}
435+
}
436+
})
437+
b.SetBytes(int64(len(codeJSON)))
438+
})
439+
b.Run("JSON", func(b *testing.B) {
440+
b.RunParallel(func(pb *testing.PB) {
441+
for pb.Next() {
442+
if _, err := json.Marshal(&codeStruct); err != nil {
443+
b.Fatal("json.Marshal:", err)
444+
}
445+
}
446+
})
447+
b.SetBytes(int64(len(codeJSON)))
448+
})
449+
}

bson/bsoncodec/codec_cache.go

+151
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
package bsoncodec
2+
3+
import (
4+
"reflect"
5+
"sync"
6+
"sync/atomic"
7+
)
8+
9+
// statically assert array size
10+
var _ = (kindEncoderCache{}).entries[reflect.UnsafePointer]
11+
var _ = (kindDecoderCache{}).entries[reflect.UnsafePointer]
12+
13+
type typeEncoderCache struct {
14+
cache sync.Map // map[reflect.Type]ValueEncoder
15+
}
16+
17+
func (c *typeEncoderCache) Store(rt reflect.Type, enc ValueEncoder) {
18+
c.cache.Store(rt, enc)
19+
}
20+
21+
func (c *typeEncoderCache) Load(rt reflect.Type) (ValueEncoder, bool) {
22+
if v, _ := c.cache.Load(rt); v != nil {
23+
return v.(ValueEncoder), true
24+
}
25+
return nil, false
26+
}
27+
28+
func (c *typeEncoderCache) LoadOrStore(rt reflect.Type, enc ValueEncoder) ValueEncoder {
29+
if v, loaded := c.cache.LoadOrStore(rt, enc); loaded {
30+
enc = v.(ValueEncoder)
31+
}
32+
return enc
33+
}
34+
35+
func (c *typeEncoderCache) Clone() *typeEncoderCache {
36+
cc := new(typeEncoderCache)
37+
c.cache.Range(func(k, v interface{}) bool {
38+
if k != nil && v != nil {
39+
cc.cache.Store(k, v)
40+
}
41+
return true
42+
})
43+
return cc
44+
}
45+
46+
type typeDecoderCache struct {
47+
cache sync.Map // map[reflect.Type]ValueDecoder
48+
}
49+
50+
func (c *typeDecoderCache) Store(rt reflect.Type, dec ValueDecoder) {
51+
c.cache.Store(rt, dec)
52+
}
53+
54+
func (c *typeDecoderCache) Load(rt reflect.Type) (ValueDecoder, bool) {
55+
if v, _ := c.cache.Load(rt); v != nil {
56+
return v.(ValueDecoder), true
57+
}
58+
return nil, false
59+
}
60+
61+
func (c *typeDecoderCache) LoadOrStore(rt reflect.Type, dec ValueDecoder) ValueDecoder {
62+
if v, loaded := c.cache.LoadOrStore(rt, dec); loaded {
63+
dec = v.(ValueDecoder)
64+
}
65+
return dec
66+
}
67+
68+
func (c *typeDecoderCache) Clone() *typeDecoderCache {
69+
cc := new(typeDecoderCache)
70+
c.cache.Range(func(k, v interface{}) bool {
71+
if k != nil && v != nil {
72+
cc.cache.Store(k, v)
73+
}
74+
return true
75+
})
76+
return cc
77+
}
78+
79+
// atomic.Value requires that all calls to Store() have the same concrete type
80+
// so we wrap the ValueEncoder with a kindEncoderCacheEntry to ensure the type
81+
// is always the same (since different concrete types may implement the
82+
// ValueEncoder interface).
83+
type kindEncoderCacheEntry struct {
84+
enc ValueEncoder
85+
}
86+
87+
type kindEncoderCache struct {
88+
entries [reflect.UnsafePointer + 1]atomic.Value // *kindEncoderCacheEntry
89+
}
90+
91+
func (c *kindEncoderCache) Store(rt reflect.Kind, enc ValueEncoder) {
92+
if enc != nil && rt < reflect.Kind(len(c.entries)) {
93+
c.entries[rt].Store(&kindEncoderCacheEntry{enc: enc})
94+
}
95+
}
96+
97+
func (c *kindEncoderCache) Load(rt reflect.Kind) (ValueEncoder, bool) {
98+
if rt < reflect.Kind(len(c.entries)) {
99+
if ent, ok := c.entries[rt].Load().(*kindEncoderCacheEntry); ok {
100+
return ent.enc, ent.enc != nil
101+
}
102+
}
103+
return nil, false
104+
}
105+
106+
func (c *kindEncoderCache) Clone() *kindEncoderCache {
107+
cc := new(kindEncoderCache)
108+
for i, v := range c.entries {
109+
if val := v.Load(); val != nil {
110+
cc.entries[i].Store(val)
111+
}
112+
}
113+
return cc
114+
}
115+
116+
// atomic.Value requires that all calls to Store() have the same concrete type
117+
// so we wrap the ValueDecoder with a kindDecoderCacheEntry to ensure the type
118+
// is always the same (since different concrete types may implement the
119+
// ValueDecoder interface).
120+
type kindDecoderCacheEntry struct {
121+
dec ValueDecoder
122+
}
123+
124+
type kindDecoderCache struct {
125+
entries [reflect.UnsafePointer + 1]atomic.Value // *kindDecoderCacheEntry
126+
}
127+
128+
func (c *kindDecoderCache) Store(rt reflect.Kind, dec ValueDecoder) {
129+
if rt < reflect.Kind(len(c.entries)) {
130+
c.entries[rt].Store(&kindDecoderCacheEntry{dec: dec})
131+
}
132+
}
133+
134+
func (c *kindDecoderCache) Load(rt reflect.Kind) (ValueDecoder, bool) {
135+
if rt < reflect.Kind(len(c.entries)) {
136+
if ent, ok := c.entries[rt].Load().(*kindDecoderCacheEntry); ok {
137+
return ent.dec, ent.dec != nil
138+
}
139+
}
140+
return nil, false
141+
}
142+
143+
func (c *kindDecoderCache) Clone() *kindDecoderCache {
144+
cc := new(kindDecoderCache)
145+
for i, v := range c.entries {
146+
if val := v.Load(); val != nil {
147+
cc.entries[i].Store(val)
148+
}
149+
}
150+
return cc
151+
}

0 commit comments

Comments
 (0)