-
Notifications
You must be signed in to change notification settings - Fork 916
GODRIVER-2914 bson: improve marshal/unmarshal performance by ~58% and ~29% #1313
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
db543ec
7ee2919
7bb0482
9de8c88
2d87bc2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,166 @@ | ||
// Copyright (C) MongoDB, Inc. 2017-present. | ||
// | ||
// Licensed under the Apache License, Version 2.0 (the "License"); you may | ||
// not use this file except in compliance with the License. You may obtain | ||
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 | ||
|
||
package bsoncodec | ||
|
||
import ( | ||
"reflect" | ||
"sync" | ||
"sync/atomic" | ||
) | ||
|
||
// Runtime check that the kind encoder and decoder caches can store any valid | ||
// reflect.Kind constant. | ||
func init() { | ||
if s := reflect.Kind(len(kindEncoderCache{}.entries)).String(); s != "kind27" { | ||
panic("The capacity of kindEncoderCache is too small.\n" + | ||
"This is due to a new type being added to reflect.Kind.") | ||
} | ||
} | ||
|
||
// statically assert array size | ||
var _ = (kindEncoderCache{}).entries[reflect.UnsafePointer] | ||
var _ = (kindDecoderCache{}).entries[reflect.UnsafePointer] | ||
|
||
type typeEncoderCache struct { | ||
cache sync.Map // map[reflect.Type]ValueEncoder | ||
} | ||
|
||
func (c *typeEncoderCache) Store(rt reflect.Type, enc ValueEncoder) { | ||
c.cache.Store(rt, enc) | ||
} | ||
|
||
func (c *typeEncoderCache) Load(rt reflect.Type) (ValueEncoder, bool) { | ||
if v, _ := c.cache.Load(rt); v != nil { | ||
return v.(ValueEncoder), true | ||
} | ||
return nil, false | ||
} | ||
|
||
func (c *typeEncoderCache) LoadOrStore(rt reflect.Type, enc ValueEncoder) ValueEncoder { | ||
if v, loaded := c.cache.LoadOrStore(rt, enc); loaded { | ||
enc = v.(ValueEncoder) | ||
} | ||
return enc | ||
} | ||
|
||
func (c *typeEncoderCache) Clone() *typeEncoderCache { | ||
cc := new(typeEncoderCache) | ||
c.cache.Range(func(k, v interface{}) bool { | ||
if k != nil && v != nil { | ||
cc.cache.Store(k, v) | ||
} | ||
return true | ||
}) | ||
return cc | ||
} | ||
|
||
type typeDecoderCache struct { | ||
cache sync.Map // map[reflect.Type]ValueDecoder | ||
} | ||
|
||
func (c *typeDecoderCache) Store(rt reflect.Type, dec ValueDecoder) { | ||
c.cache.Store(rt, dec) | ||
} | ||
|
||
func (c *typeDecoderCache) Load(rt reflect.Type) (ValueDecoder, bool) { | ||
if v, _ := c.cache.Load(rt); v != nil { | ||
return v.(ValueDecoder), true | ||
} | ||
return nil, false | ||
} | ||
|
||
func (c *typeDecoderCache) LoadOrStore(rt reflect.Type, dec ValueDecoder) ValueDecoder { | ||
if v, loaded := c.cache.LoadOrStore(rt, dec); loaded { | ||
dec = v.(ValueDecoder) | ||
} | ||
return dec | ||
} | ||
|
||
func (c *typeDecoderCache) Clone() *typeDecoderCache { | ||
cc := new(typeDecoderCache) | ||
c.cache.Range(func(k, v interface{}) bool { | ||
if k != nil && v != nil { | ||
cc.cache.Store(k, v) | ||
} | ||
return true | ||
}) | ||
return cc | ||
} | ||
|
||
// atomic.Value requires that all calls to Store() have the same concrete type | ||
// so we wrap the ValueEncoder with a kindEncoderCacheEntry to ensure the type | ||
// is always the same (since different concrete types may implement the | ||
// ValueEncoder interface). | ||
type kindEncoderCacheEntry struct { | ||
enc ValueEncoder | ||
} | ||
|
||
type kindEncoderCache struct { | ||
entries [reflect.UnsafePointer + 1]atomic.Value // *kindEncoderCacheEntry | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Using an array sized by That suggestion goes for Here's the benchmark comparison between using a
Edit: Here's a gist with the changes used to get those results. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I can go either way with this since we quickly store the result returned by the That said, it is a lot faster when benchmarked in isolation and we do test against a new type being added, which shouldn't be possible until Go 2.0 and the last time
func BenchmarkKindEncoderCacheLoad(b *testing.B) {
m := new(sync.Map)
x := make(map[reflect.Kind]ValueEncoder)
c := new(kindEncoderCache)
for k := reflect.Kind(0); k <= reflect.UnsafePointer; k++ {
c.Store(k, fakeCodec{})
m.Store(k, fakeCodec{})
x[k] = fakeCodec{}
}
b.Run("Array", func(b *testing.B) {
for i := 0; i < b.N; i++ {
kind := reflect.Kind(i) % (reflect.UnsafePointer + 1)
_, ok := c.Load(kind)
if !ok {
b.Fatal("missing:", kind)
}
}
})
b.Run("SyncMap", func(b *testing.B) {
for i := 0; i < b.N; i++ {
kind := reflect.Kind(i) % (reflect.UnsafePointer + 1)
_, ok := m.Load(kind)
if !ok {
b.Fatal("missing:", kind)
}
}
})
// Map listed here because this is potentially read-only and could be represented as a plain map.
b.Run("Map", func(b *testing.B) {
for i := 0; i < b.N; i++ {
kind := reflect.Kind(i) % (reflect.UnsafePointer + 1)
_, ok := x[kind]
if !ok {
b.Fatal("wat")
}
}
})
} There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. My concern is that a user uses an old driver against a new Go with a reflect kind greater than
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed with: 924008c |
||
} | ||
|
||
func (c *kindEncoderCache) Store(rt reflect.Kind, enc ValueEncoder) { | ||
if enc != nil && rt < reflect.Kind(len(c.entries)) { | ||
c.entries[rt].Store(&kindEncoderCacheEntry{enc: enc}) | ||
} | ||
} | ||
|
||
func (c *kindEncoderCache) Load(rt reflect.Kind) (ValueEncoder, bool) { | ||
if rt < reflect.Kind(len(c.entries)) { | ||
if ent, ok := c.entries[rt].Load().(*kindEncoderCacheEntry); ok { | ||
return ent.enc, ent.enc != nil | ||
} | ||
} | ||
return nil, false | ||
} | ||
|
||
func (c *kindEncoderCache) Clone() *kindEncoderCache { | ||
cc := new(kindEncoderCache) | ||
for i, v := range c.entries { | ||
if val := v.Load(); val != nil { | ||
cc.entries[i].Store(val) | ||
} | ||
} | ||
return cc | ||
} | ||
|
||
// atomic.Value requires that all calls to Store() have the same concrete type | ||
// so we wrap the ValueDecoder with a kindDecoderCacheEntry to ensure the type | ||
// is always the same (since different concrete types may implement the | ||
// ValueDecoder interface). | ||
type kindDecoderCacheEntry struct { | ||
dec ValueDecoder | ||
} | ||
|
||
type kindDecoderCache struct { | ||
entries [reflect.UnsafePointer + 1]atomic.Value // *kindDecoderCacheEntry | ||
} | ||
|
||
func (c *kindDecoderCache) Store(rt reflect.Kind, dec ValueDecoder) { | ||
if rt < reflect.Kind(len(c.entries)) { | ||
c.entries[rt].Store(&kindDecoderCacheEntry{dec: dec}) | ||
} | ||
} | ||
|
||
func (c *kindDecoderCache) Load(rt reflect.Kind) (ValueDecoder, bool) { | ||
if rt < reflect.Kind(len(c.entries)) { | ||
if ent, ok := c.entries[rt].Load().(*kindDecoderCacheEntry); ok { | ||
return ent.dec, ent.dec != nil | ||
} | ||
} | ||
return nil, false | ||
} | ||
|
||
func (c *kindDecoderCache) Clone() *kindDecoderCache { | ||
cc := new(kindDecoderCache) | ||
for i, v := range c.entries { | ||
if val := v.Load(); val != nil { | ||
cc.entries[i].Store(val) | ||
} | ||
} | ||
return cc | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this use the byte slice returned by
json.Marshal
instead?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The output of
json.Marshal
andcodeJSON
are identical (we test for this when initializing these values):mongo-go-driver/bson/benchmark_test.go
Lines 369 to 377 in 9de8c88
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, thanks for the update! Sounds good.