-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Improve coalesce and concat performance for views
#7614
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
c3648f4
d9d309f
cda04cf
675d9d6
b44d0ab
294e188
9936a8a
abe62b9
ab2878e
06101a1
7b408ad
16ff18c
97d93aa
9878106
c422216
7a50614
d2305b9
a94a096
cf26d86
289aeda
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 |
|---|---|---|
|
|
@@ -21,9 +21,9 @@ | |
| //! [`filter`]: crate::filter::filter | ||
| //! [`take`]: crate::take::take | ||
| use crate::concat::concat_batches; | ||
| use arrow_array::{ | ||
| builder::StringViewBuilder, cast::AsArray, Array, ArrayRef, RecordBatch, RecordBatchOptions, | ||
| }; | ||
| use arrow_array::StringViewArray; | ||
| use arrow_array::{cast::AsArray, Array, ArrayRef, RecordBatch}; | ||
| use arrow_data::ByteView; | ||
| use arrow_schema::{ArrowError, SchemaRef}; | ||
| use std::collections::VecDeque; | ||
| use std::sync::Arc; | ||
|
|
@@ -164,7 +164,7 @@ impl BatchCoalescer { | |
| return Ok(()); | ||
| } | ||
|
|
||
| let mut batch = gc_string_view_batch(&batch); | ||
| let mut batch = gc_string_view_batch(batch); | ||
|
|
||
| // If pushing this batch would exceed the target batch size, | ||
| // finish the current batch and start a new one | ||
|
|
@@ -242,15 +242,19 @@ impl BatchCoalescer { | |
| /// However, after a while (e.g., after `FilterExec` or `HashJoinExec`) the | ||
| /// `StringViewArray` may only refer to a small portion of the buffer, | ||
| /// significantly increasing memory usage. | ||
| fn gc_string_view_batch(batch: &RecordBatch) -> RecordBatch { | ||
| let new_columns: Vec<ArrayRef> = batch | ||
| .columns() | ||
| .iter() | ||
| fn gc_string_view_batch(batch: RecordBatch) -> RecordBatch { | ||
| let (schema, columns, num_rows) = batch.into_parts(); | ||
| let new_columns: Vec<ArrayRef> = columns | ||
|
Contributor
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 don't know if it matters but in #7620 I simply modified the existing Vec rather than collecting into a new one to save maybe an allocation for mut c in columns.iter_mut() {
...
}
Contributor
Author
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. AFAIK there is a Rust compiler optimization that will reuse a
Contributor
Author
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 one of the reasons I think using
Contributor
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 I think using |
||
| .into_iter() | ||
| .map(|c| { | ||
| // Try to re-create the `StringViewArray` to prevent holding the underlying buffer too long. | ||
| let Some(s) = c.as_string_view_opt() else { | ||
| return Arc::clone(c); | ||
| return c; | ||
| }; | ||
| if s.data_buffers().is_empty() { | ||
| // If there are no data buffers, we can just return the array as is | ||
| return c; | ||
| } | ||
| let ideal_buffer_size: usize = s | ||
| .views() | ||
| .iter() | ||
|
|
@@ -264,42 +268,73 @@ fn gc_string_view_batch(batch: &RecordBatch) -> RecordBatch { | |
| }) | ||
| .sum(); | ||
| let actual_buffer_size = s.get_buffer_memory_size(); | ||
| let buffers = s.data_buffers(); | ||
|
|
||
| // Re-creating the array copies data and can be time consuming. | ||
| // We only do it if the array is sparse | ||
| if actual_buffer_size > (ideal_buffer_size * 2) { | ||
| if ideal_buffer_size == 0 { | ||
| // If the ideal buffer size is 0, all views are inlined | ||
| // so just reuse the views | ||
| return Arc::new(unsafe { | ||
| StringViewArray::new_unchecked( | ||
| s.views().clone(), | ||
| vec![], | ||
| s.nulls().cloned(), | ||
| ) | ||
| }); | ||
| } | ||
| // We set the block size to `ideal_buffer_size` so that the new StringViewArray only has one buffer, which accelerate later concat_batches. | ||
| // See https://github.com/apache/arrow-rs/issues/6094 for more details. | ||
| let mut builder = StringViewBuilder::with_capacity(s.len()); | ||
| if ideal_buffer_size > 0 { | ||
| builder = builder.with_fixed_block_size(ideal_buffer_size as u32); | ||
| } | ||
|
|
||
| for v in s.iter() { | ||
| builder.append_option(v); | ||
| } | ||
|
|
||
| let gc_string = builder.finish(); | ||
|
|
||
| debug_assert!(gc_string.data_buffers().len() <= 1); // buffer count can be 0 if the `ideal_buffer_size` is 0 | ||
| let mut buffer: Vec<u8> = Vec::with_capacity(ideal_buffer_size); | ||
|
|
||
| let views: Vec<u128> = s | ||
| .views() | ||
| .as_ref() | ||
| .iter() | ||
| .cloned() | ||
| .map(|v| { | ||
| let mut b: ByteView = ByteView::from(v); | ||
|
|
||
| if b.length > 12 { | ||
| let offset = buffer.len() as u32; | ||
| buffer.extend_from_slice( | ||
| buffers[b.buffer_index as usize] | ||
| .get(b.offset as usize..b.offset as usize + b.length as usize) | ||
| .expect("Invalid buffer slice"), | ||
| ); | ||
| b.offset = offset; | ||
| b.buffer_index = 0; // Set buffer index to 0, as we only have one buffer | ||
| } | ||
|
|
||
| b.into() | ||
| }) | ||
| .collect(); | ||
|
Comment on lines
+291
to
+312
Contributor
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'm curious it is better if implement like this: let mut views = Vec::with_capacity(s.views().len());
for &v in s.views().as_ref() {
let mut b: ByteView = ByteView::from(v);
if b.length > 12 {
...
}
views.push(b.into());
}vector pre-allocates and no clone 🤔
Contributor
Author
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. No that is generally worse -
the |
||
|
|
||
| let buffers = if buffer.is_empty() { | ||
|
Contributor
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 think the only way this could happen is if if ideal_buffer_size == 0 {
// just reuse views as is...
}
Contributor
Author
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. Added 🚀 |
||
| vec![] | ||
| } else { | ||
| vec![buffer.into()] | ||
| }; | ||
|
|
||
| let gc_string = unsafe { | ||
| StringViewArray::new_unchecked(views.into(), buffers, s.nulls().cloned()) | ||
| }; | ||
|
|
||
| Arc::new(gc_string) | ||
| } else { | ||
| Arc::clone(c) | ||
| c | ||
| } | ||
| }) | ||
| .collect(); | ||
| let mut options = RecordBatchOptions::new(); | ||
| options = options.with_row_count(Some(batch.num_rows())); | ||
| RecordBatch::try_new_with_options(batch.schema(), new_columns, &options) | ||
| .expect("Failed to re-create the gc'ed record batch") | ||
| unsafe { RecordBatch::new_unchecked(schema, new_columns, num_rows) } | ||
| } | ||
|
|
||
| #[cfg(test)] | ||
| mod tests { | ||
| use super::*; | ||
| use arrow_array::builder::ArrayBuilder; | ||
| use arrow_array::{StringViewArray, UInt32Array}; | ||
| use arrow_array::builder::{ArrayBuilder, StringViewBuilder}; | ||
| use arrow_array::{RecordBatchOptions, StringViewArray, UInt32Array}; | ||
| use arrow_schema::{DataType, Field, Schema}; | ||
| use std::ops::Range; | ||
|
|
||
|
|
@@ -518,9 +553,11 @@ mod tests { | |
| fn test_gc_string_view_test_batch_empty() { | ||
| let schema = Schema::empty(); | ||
| let batch = RecordBatch::new_empty(schema.into()); | ||
| let output_batch = gc_string_view_batch(&batch); | ||
| assert_eq!(batch.num_columns(), output_batch.num_columns()); | ||
| assert_eq!(batch.num_rows(), output_batch.num_rows()); | ||
| let cols = batch.num_columns(); | ||
| let num_rows = batch.num_rows(); | ||
| let output_batch = gc_string_view_batch(batch); | ||
| assert_eq!(cols, output_batch.num_columns()); | ||
| assert_eq!(num_rows, output_batch.num_rows()); | ||
| } | ||
|
|
||
| #[test] | ||
|
|
@@ -568,9 +605,11 @@ mod tests { | |
| /// and ensures the number of rows are the same | ||
| fn do_gc(array: StringViewArray) -> StringViewArray { | ||
| let batch = RecordBatch::try_from_iter(vec![("a", Arc::new(array) as ArrayRef)]).unwrap(); | ||
| let gc_batch = gc_string_view_batch(&batch); | ||
| assert_eq!(batch.num_rows(), gc_batch.num_rows()); | ||
| assert_eq!(batch.schema(), gc_batch.schema()); | ||
| let rows = batch.num_rows(); | ||
| let schema = batch.schema(); | ||
| let gc_batch = gc_string_view_batch(batch); | ||
| assert_eq!(rows, gc_batch.num_rows()); | ||
| assert_eq!(schema, gc_batch.schema()); | ||
| gc_batch | ||
| .column(0) | ||
| .as_any() | ||
|
|
||
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 a great idea 💯