-
Notifications
You must be signed in to change notification settings - Fork 892
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
GODRIVER-2914 bsoncodec/bsonrw: eliminate encoding allocations #1323
Changes from 2 commits
f9c485a
4beb9fe
80680c8
603bb4a
6c41835
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 |
---|---|---|
|
@@ -9,6 +9,7 @@ package bson | |
import ( | ||
"bytes" | ||
"encoding/json" | ||
"sync" | ||
|
||
"go.mongodb.org/mongo-driver/bson/bsoncodec" | ||
"go.mongodb.org/mongo-driver/bson/bsonrw" | ||
|
@@ -141,6 +142,13 @@ func MarshalAppendWithRegistry(r *bsoncodec.Registry, dst []byte, val interface{ | |
return MarshalAppendWithContext(bsoncodec.EncodeContext{Registry: r}, dst, val) | ||
} | ||
|
||
// Pool of buffers for marshalling BSON. | ||
var bufPool = sync.Pool{ | ||
New: func() interface{} { | ||
return new(bytes.Buffer) | ||
}, | ||
} | ||
|
||
// MarshalAppendWithContext will encode val as a BSON document using Registry r and EncodeContext ec and append the | ||
// 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 | ||
// transformed into a document, MarshalValueAppendWithContext should be used instead. | ||
|
@@ -162,8 +170,26 @@ func MarshalAppendWithRegistry(r *bsoncodec.Registry, dst []byte, val interface{ | |
// | ||
// See [Encoder] for more examples. | ||
func MarshalAppendWithContext(ec bsoncodec.EncodeContext, dst []byte, val interface{}) ([]byte, error) { | ||
sw := new(bsonrw.SliceWriter) | ||
*sw = dst | ||
sw := bufPool.Get().(*bytes.Buffer) | ||
defer func() { | ||
// Proper usage of a sync.Pool requires each entry to have approximately | ||
// the same memory cost. To obtain this property when the stored type | ||
// contains a variably-sized buffer, we add a hard limit on the maximum | ||
// buffer to place back in the pool. We limit the size to 16MiB because | ||
// that's the maximum wire message size supported by any current MongoDB | ||
// server. | ||
// | ||
// Comment based on | ||
// https://cs.opensource.google/go/go/+/refs/tags/go1.19:src/fmt/print.go;l=147 | ||
// | ||
// Recycle byte slices that are smaller than 16MiB and at least half | ||
// occupied. | ||
if sw.Cap() < 16*1024*1024 && sw.Cap()/2 < sw.Len() { | ||
bufPool.Put(sw) | ||
} | ||
}() | ||
|
||
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. This is normally best practice, but the Go standard library's "encoding/json" package does not perform this check when returning If we do keep this logic then we should probably only check the capacity since I'm not sure what the check for 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. Yep, the current "encoding/json" library doesn't do any pooled buffer size checking, which is actually the subject of golang/go#27735. We got the idea to measure buffer utilization as a simple way to prevent holding onto a bunch of mostly unused memory from that issue. Under ideal usage patterns where messages are all the same size, the pooled memory will be mostly utilized. The pathological case is when there are mostly small messages with a few very large messages. That can cause the pooled buffers to grow very large, holding onto a lot of memory that is underutilized by the mostly small messages. That issue happened for some customers after we added buffer pooling to the operation logic and lead to GODRIVER-2677. The Go "encoding/json" library can currently also suffer from the same memory pooling problem. One possible solution is to keep "buckets" of buffers of different size, so large buffers are used for large messages and small buffers are used for small messages. However, the implementation for that is relatively complex, so we decided to go with the simpler solution of setting a maximum buffer size and minimum utilization inspired by this comment. The trade off is holding onto less unused memory but possibly allocating memory more often for large messages. 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. Thank you for pointing out that issue. I think the easiest safe thing to do would be to increase the size limit of the buffers that we'll pool to something like 32mb (2x the max size of BSON docs or even 24mb) and remove the check to see if half the capacity is used. The motivation here is that due to how We should also check if the size is equal to or less than whatever limit we pick.. Also, this isn't that big of a deal since the difference here will likely only be an allocation or two. |
||
sw.Reset() | ||
vw := bvwPool.Get(sw) | ||
defer bvwPool.Put(vw) | ||
|
||
|
@@ -184,7 +210,7 @@ func MarshalAppendWithContext(ec bsoncodec.EncodeContext, dst []byte, val interf | |
return nil, err | ||
} | ||
|
||
return *sw, nil | ||
return append(dst, sw.Bytes()...), nil | ||
} | ||
|
||
// MarshalValue returns the BSON encoding of val. | ||
|
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.
Optional: Consider adding a comment about why we can check individual bytes rather than runes.
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.
Real reason is that
'\x00'
is a byte (it equals0
not00000000
) and since it is less that MaxRune (0x80
) it just callsIndexByte
- this change just omits an unnecessary function call (and one that can't currently be inlined because its complexity score exceeds what is allowed for inlining).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.
I think we're both saying the same thing.
0x80
(hex) ==10000000
(binary). I can update the comment with a reference to the corresponding code instrings.ContainsRune
for additional clarity.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, I thought you meant
10000000
as hex or a 32bit rune here since that's what IndexRune takes.