-
Notifications
You must be signed in to change notification settings - Fork 178
RUST-536 Eliminate redundant clones #377
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
RUST-536 Eliminate redundant clones #377
Conversation
@@ -171,7 +166,7 @@ where | |||
.ok_or_else(|| D::Error::custom(format!("could not deserialize u64 from {:?}", bson))) | |||
} | |||
|
|||
pub fn doc_size_bytes(doc: &Document) -> usize { | |||
pub fn doc_size_bytes(doc: &Document) -> u64 { |
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.
this was changed to always use a 64-bit integer in case we're on a 32-bit system and a large document is encountered.
/// Maximum size in bytes of an insert batch. | ||
/// This is intentionally less than the actual max document size, which is 16*1024*1024 bytes, to | ||
/// allow for overhead in the command document. | ||
const MAX_INSERT_DOCS_BYTES: usize = 16 * 1000 * 1000; |
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.
we previously had a hard-coded value for this, but we're actually meant to derive this from the hello
response. Also, we baked in a healthy ~5% overhead allotment here, but there is already 16 KiB of overhead allowed on top of the value returned from hello
, so imposing our own wiggle room isn't necessary.
src/coll/mod.rs
Outdated
&self, | ||
docs: impl IntoIterator<Item = impl Borrow<T>>, | ||
docs: impl IntoIterator<Item = D>, |
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.
We need an explicit generic type here in order to collect the things being borrowed into a Vec
, which is needed to ensure that the references live long enough.
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.
Another option would be let ds: Vec<_> = ...
, I think?
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 neat, that does work! I wasn't aware this was possible with opaque types. Updated to use that.
let current_batch = remaining_docs; | ||
while n_attempted < ds.len() { | ||
let docs: Vec<&T> = ds.iter().skip(n_attempted).map(Borrow::borrow).collect(); | ||
let insert = Insert::new(self.namespace(), docs, options.clone()); |
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.
In order to eliminate clones, we pass the T
references into Insert
instead of Document
s. However, this means that at this level we can't know how many documents should be included in the batch, so we pass all of the references each time (skipping the ones completed) and need to discover the batch size from the return values of execute_operation
.
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 you can get the same elimination of clones without repeating serialization if you store a Vec<Document>
in this method and then pass a Vec<&Document>
to Insert::new
.
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 problem persists with that I think, since we wouldn't be able to move the Vec<&Document>
into the command document. By delaying serialization until building the document, we can move the documents directly and just hope we don't have to serialize again in a retry attempt.
.inserted_ids | ||
.insert(index + n_attempted - current_batch_size, id); | ||
} | ||
let current_batch_size = result.inserted_ids.len(); |
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.
If it succeeded, we know the batch size is just the number of documents that were inserted.
// for ordered inserts this size will be incorrect, but knowing the batch | ||
// size isn't needed for ordered failures since we | ||
// return immediately from them anyways. | ||
let current_batch_size = bw.inserted_ids.len() |
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.
If it failed (and the insert was not ordered), then we know the batch size is the number of inserted documents + the number of ones that failed. If it's ordered we can't figure it out from that alone, but that's okay because we're going to return early anyways and don't need to know the batch size.
@@ -510,13 +508,16 @@ pub struct BulkWriteFailure { | |||
|
|||
/// The error that occurred on account of write concern failure. | |||
pub write_concern_error: Option<WriteConcernError>, | |||
|
|||
pub(crate) inserted_ids: HashMap<usize, Bson>, |
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.
This is necessary to track how many writes were attempted in the event of an error in an unordered bulk write, but it's also something that users might find useful (RUST-260).
@@ -66,7 +66,8 @@ pub(crate) trait Operation { | |||
const NAME: &'static str; | |||
|
|||
/// Returns the command that should be sent to the server as part of this operation. | |||
fn build(&self, description: &StreamDescription) -> Result<Command>; | |||
/// The operation may store some additional state that is required for handling the response. | |||
fn build(&mut self, description: &StreamDescription) -> Result<Command>; |
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.
This was updated to take &mut self
so that Insert
could store the _id
's it appended to the documents it inserts.
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.
It's a pity that so many sites had to be changed to accommodate the single Insert
case; I wonder if it would be better to keep this as &self
and cheat with a RefCell
in Index
?
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.
Yeah, it is a bit unfortunate. My original implementation actually avoided it by introducing a build_mut
method in Operation
with a default implementation that called into build
, but that left Insert
in a weird place where it implemented build_mut
but not build
, so I just decided to bite the bullet and do a search and replace so that all operations were consistent (it actually ended up being a pretty quick refactor despite all the changes).
Regarding tricks with RefCell
or Mutex
and what not, I think it's best we not hide any of the mutability either, since it's now important that build
gets called before handle_response
, and build
being a mutating method helps signify that to some extent.
.take(description.max_write_batch_size as usize) | ||
.enumerate() | ||
{ | ||
let mut doc = bson::to_document(d)?; |
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.
By delaying the serialization until build
, we can avoid cloning here. We will have to repeat the serialization in the retry making that case slower, but the common case gets much faster.
let doc_size = bson_util::array_entry_size_bytes(i, &doc); | ||
|
||
if (size + doc_size) <= description.max_bson_object_size as u64 { | ||
if self.inserted_ids.len() <= i { |
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.
only populated the inserted_ids
vector if this is the first time build
is being called.
src/coll/mod.rs
Outdated
&self, | ||
docs: impl IntoIterator<Item = impl Borrow<T>>, | ||
docs: impl IntoIterator<Item = D>, |
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.
Another option would be let ds: Vec<_> = ...
, I think?
let current_batch = remaining_docs; | ||
while n_attempted < ds.len() { | ||
let docs: Vec<&T> = ds.iter().skip(n_attempted).map(Borrow::borrow).collect(); | ||
let insert = Insert::new(self.namespace(), docs, options.clone()); |
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 you can get the same elimination of clones without repeating serialization if you store a Vec<Document>
in this method and then pass a Vec<&Document>
to Insert::new
.
} | ||
|
||
impl BulkWriteFailure { | ||
pub(crate) fn new() -> Self { | ||
BulkWriteFailure { | ||
write_errors: None, | ||
write_concern_error: None, | ||
inserted_ids: Default::default(), |
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.
Out of curiosity, why not HashMap::new()
?
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 choice was arbitrary. I think maybe I wasn't sure if HashMap
had been imported or not so I went with Default
.
@@ -66,7 +66,8 @@ pub(crate) trait Operation { | |||
const NAME: &'static str; | |||
|
|||
/// Returns the command that should be sent to the server as part of this operation. | |||
fn build(&self, description: &StreamDescription) -> Result<Command>; | |||
/// The operation may store some additional state that is required for handling the response. | |||
fn build(&mut self, description: &StreamDescription) -> Result<Command>; |
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.
It's a pity that so many sites had to be changed to accommodate the single Insert
case; I wonder if it would be better to keep this as &self
and cheat with a RefCell
in Index
?
RUST-536
This PR removes the need to clone input documents or command responses in the driver, resulting in significant performance improvements (from 15-40% depending on the benchmark). It also removes unneeded clones that were used in the benchmark runner now that references can be accepted in the
insert
family of methods (#331). The benchmark runner was also updated to compile, as it had slipped out of date with the breaking changes in the driver / tokio.