-
Notifications
You must be signed in to change notification settings - Fork 974
Convert JSON to VariantArray without copying (8 - 32% faster) #7911
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
base: main
Are you sure you want to change the base?
Conversation
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 remarkably simpler than I had imagined it would need to be. Handing off ownership back and forth was a very useful trick.
My only concern is whether we might ever need to support a builder that isn't backed by Vec
? I'm guessing not, but wanted to double check.
I think eventually we might, but I think the only way to really do so is via some sort of trait and a templated builder. I think we can pretty far without doing so and And there are zero copy APIs to/from Vec for the underlying Arrow arrays which I think is a pretty nice property too |
bb1502a
to
e07069d
Compare
… buffers (#7912) # Which issue does this PR close? - closes #7805 - part of #6736 - part of #7911 # Rationale for this change I would like to be able to write Variants directly into the target buffer when writing multiple variants However, the current VariantBuilder allocates a new bufffer for each variant # What changes are included in this PR? 1. Add `VariantBuilder::new_with_buffers` and docs and tests You can see how this API can be used to write directly into a buffer in VariantArrayBuilder in this PR: - #7911 # Are these changes tested? Yes new tests # Are there any user-facing changes? New API
e07069d
to
8166cb6
Compare
8166cb6
to
e10d41d
Compare
Update here is I think I have incorporated @scovich's comments and I am quite pleased with how it is looking I think this code needs a few more tests and a benchmark or two and we'll be good. I'll try and work on those in the next few days |
impl<'a> Drop for VariantArrayVariantBuilder<'a> { | ||
/// If the builder was not finished, roll back any changes made to the | ||
/// underlying buffers (by truncating them) | ||
fn drop(&mut self) { |
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 really like this approach. I was thinking over the weekend that we may want to rework the other builders to follow a similar approach:
- They can truncate the metadata dictionary on rollback, which would eliminate the false allocations that survive a rollback today
- We can allocate the value bytes directly in the base buffer (instead of using a separate Vec)
- On rollback, just
truncate
(like here) - On success, use Vec::splice to insert value offset and field id arrays, which slides over all the other bytes
- On rollback, just
- Once we're using
splice
, it opens the door to pre-allocate the space for the value offset and field arrays, in case the caller knows how many fields or array elements there are.- If the prediction was correct,
splice
just replaces the pre-allocated space. - If incorrect, the pre-allocation is wasted (but we're no worse off than before -- the bytes just inject in)
- The main complication would be guessing how many bytes to encode each offset with.
- If the prediction was correct,
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.
They can truncate the metadata dictionary on rollback, which would eliminate the false allocations that survive a rollback today
That is an excellent point
We can allocate the value bytes directly in the base buffer (instead of using a separate Vec)
That sounds like a great way to avoid the extra allocation
Once we're using splice, it opens the door to pre-allocate the space for the value offset and field arrays, in case the caller knows how many fields or array elements there are.
This is also a great idea 🤯
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.
As a follow up, @klion26 has a PR up to implement this:
I added some benchmarks and my local results suggest that avoiding the allocations makes parsing small repeated json objects about 10% faster. I think once we stop copying stuff around in the sub builders, the other bencmarks will be quite a bit faster too
|
@@ -1047,16 +1047,16 @@ impl Drop for ObjectBuilder<'_> { | |||
/// | |||
/// Allows users to append values to a [`VariantBuilder`], [`ListBuilder`] or | |||
/// [`ObjectBuilder`]. using the same interface. | |||
pub trait VariantBuilderExt<'m, 'v> { | |||
fn append_value(&mut self, value: impl Into<Variant<'m, 'v>>); | |||
pub trait VariantBuilderExt { |
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.
There is no reason for the lifetimes to be attached to the trait itself -- if it is that means that the lifetimes trickle into the values -- since this trait is for actually constructing variant values (and copying the underlying bytes) I moved the lifetimes to just the arguments that need it
// TODO make this more efficient by avoiding the intermediate buffers | ||
let mut variant_builder = VariantBuilder::new(); | ||
variant_builder.append_value(variant); | ||
let (metadata, value) = variant_builder.finish(); |
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 whole point of this PR is to avoid this copy here and instead write directly into the output
30ad86b
to
3b6aef6
Compare
# Which issue does this PR close? We generally require a GitHub issue to be filed for all bug fixes and enhancements and this helps us generate change logs for our releases. You can link an issue to this PR using the GitHub syntax. - Part of #7911 - Part of #6736 - Follow on to #7905 # Rationale for this change I wrote benchmark some changes to the json decoder in #7911 but they are non trivial. To keep #7911 easier to review I have pulled the benchmarks out to their own PR # What changes are included in this PR? 1. Add new json benchmark 2. Include the `variant_get` benchmark added in #7919 by @Samyak2 # Are these changes tested? I tested them manually and clippy CI coverage ensures they compile # Are there any user-facing changes? No these are only benchmarks
3b6aef6
to
92e5c12
Compare
json_to_variant(input_string_array.value(i), &mut vb)?; | ||
let (metadata, value) = vb.finish(); |
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 whole point if this PR is to avoid this copy / append
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
92e5c12
to
fce19eb
Compare
@@ -55,9 +55,14 @@ use std::sync::Arc; | |||
/// }; | |||
/// builder.append_variant_buffers(&metadata, &value); | |||
/// | |||
/// // Use `variant_builder` method to write values directly to the output array |
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 the key new API -- a builder that can write directly to the correct output array location
impl<'a> Drop for VariantArrayVariantBuilder<'a> { | ||
/// If the builder was not finished, roll back any changes made to the | ||
/// underlying buffers (by truncating them) | ||
fn drop(&mut self) { |
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.
As a follow up, @klion26 has a PR up to implement this:
🤖 |
🤖: Benchmark completed Details
|
This one is now ready for review. I am quite pleased it already shows some benchmarks going 30% faster Along with the following PR from @klion26 I think our JSON conversion is about as fast as it is going to get until we move away from serde_json for parsing |
Which issue does this PR close?
VariantArray
andVariantArrayBuilder
for constructing Arrow Arrays of Variants #7905Rationale for this change
In a quest to have the fastest and most efficient Variant implementation I would like to avoid copies if at all possible
Right now, to make a VariantArray first requires completing an individual buffer and appending it
to the array.
Let's make that faster by having the VariantBuilder append directly into the buffer
What changes are included in this PR?
VariantBuilder::new_from_existing
VariantArrayBuilder::variant_builder
that reuses the buffersAre these changes tested?
Are there any user-facing changes?
Hopefully faster performance