-
Notifications
You must be signed in to change notification settings - Fork 868
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
Improve MutableArrayData Null Handling (#1224) (#1230) #1225
Conversation
arrow/src/array/transform/mod.rs
Outdated
.collect(); | ||
|
||
let null_buffer = if use_nulls { | ||
let (null_buffer, extend_null_bits) = if use_nulls { |
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.
use_nulls
is always true if any of the input arrays contain nulls
Benchmarks
So a little bit over 2x faster, performance for arrays containing nulls is unimpacted (as expected) |
Codecov Report
@@ Coverage Diff @@
## master #1225 +/- ##
==========================================
- Coverage 82.70% 82.70% -0.01%
==========================================
Files 175 175
Lines 51711 51714 +3
==========================================
+ Hits 42769 42771 +2
- Misses 8942 8943 +1
Continue to review full report at Codecov.
|
Nice, I think this should also speed up the |
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.
Nice find -- I checked that self.extend_null_bits isn't used anywhere else 👍
I just need to double-check how this interacts with ExtendNulls |
Applied the same treatment to ExtendNulls. FWIW I think this was a bug before, as you could call extend_nulls on a MutableArrayData with null handling disabled and end up with an inconsistent final ArrayData |
@@ -608,18 +614,28 @@ impl<'a> MutableArrayData<'a> { | |||
/// This function panics if the range is out of bounds, i.e. if `start + len >= array.len()`. | |||
pub fn extend(&mut self, index: usize, start: usize, end: usize) { | |||
let len = end - start; | |||
(self.extend_null_bits[index])(&mut self.data, start, len); | |||
if !self.extend_null_bits.is_empty() { | |||
(self.extend_null_bits[index])(&mut self.data, start, 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.
I think the name of the use_nulls
parameter is confusing and is not necessarily related to whether nulls are present or not, here is an excerpt of the method's doc comment:
use_nulls
is a flag used to optimize insertions. It should befalse
if the only source of nulls are the arrays themselves
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.
looking at build_extend_null_bits
this is already optimized by returning a no-op function ( Box::new(|_, _, _| {})
) if use_nulls
is false and the array doesn't have a null bitmap
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.
Yes so if it is true it cannot assume that a bitmask won't be needed due to a call to extend_nulls. Otherwise if it is false and the arrays don't contain nulls, it knows it doesn't need to compute a null bitmask?
At least that was my reading of it, although the fact you can call extend_nulls having specified use_nulls as false and get something other than a panic suggests maybe I'm missing something 😅
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.
no-op function ( Box::new(|_, _, _| {}) ) if use_nulls is false and the array doesn't have a null bitmap
This is a good point, the particular case the filter benchmarks hit is where the array has a null bitmap, but a zero null count. So an alternative fix might be to fix extend_null_bits
. Although now that I think about it, I'm not sure that no-op function is correct in the event of mixed array nullability 🤔
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.
my reading of the code (and the little documentation that accompanies it) is that use_nulls
is used to determine where the nulls will be coming from, not whether nulls are present or not
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 would agree with you if it weren't for this code
// if any of the arrays has nulls, insertions from any array requires setting bits
// as there is at least one array with nulls.
if arrays.iter().any(|array| array.null_count() > 0) {
use_nulls = true;
};
Effectively it changes meaning part way through 😅
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.
yes, I agree - this needs clarification or fixing, to make it less confusing
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 believe @tustvold has resolved this in the latest version of this PR -- by making it clear that use_nulls
must be set to true
otherwise a panic
will result if the buffer is extended with nulls
I've pushed a first cut of trying to clarify the null handling in MutableArrayData. This PR also now fixes #1230. It is worth highlighting that this is an API change. In particular until #1234 is merged the tests will fail. I also changed to using BooleanBufferBuilder instead of MutableBuffer directly, which gives an extra sanity check and also avoids some code duplication. This required making I will take another look at this with fresh eyes in the morning. |
} | ||
|
||
array_data_builder | ||
} | ||
} | ||
|
||
fn build_extend_null_bits(array: &ArrayData, use_nulls: bool) -> ExtendNullBits { |
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.
Rather than returning a no-op if use_nulls is false, simply don't construct. An ExtendNullBits that does nothing feels like a potential footgun that is better to handle in a way that will fail loudly
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 it looks good. Thank you @tustvold
@yordan-pavlov any other thoughts?
/// be computed only if `arrays` contains nulls. | ||
/// | ||
/// Code that plans to call [MutableArrayData::extend_nulls] MUST set `use_nulls` to `true`, | ||
/// in order to ensure that a null bitmap is computed. |
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 ensure that a null bitmap is computed. | |
/// in order to ensure that a null bitmap is computed, otherwise a panic will result. |
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 it will panic otherwise, right? Rather than get undefined behavior?
} else { | ||
// create 0 capacity mutable buffer with the intention that it won't be used | ||
MutableBuffer::with_capacity(0) | ||
// create no null buffer and no extend_null_bits |
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.
nice 👍
@@ -608,18 +614,28 @@ impl<'a> MutableArrayData<'a> { | |||
/// This function panics if the range is out of bounds, i.e. if `start + len >= array.len()`. | |||
pub fn extend(&mut self, index: usize, start: usize, end: usize) { | |||
let len = end - start; | |||
(self.extend_null_bits[index])(&mut self.data, start, len); | |||
if !self.extend_null_bits.is_empty() { | |||
(self.extend_null_bits[index])(&mut self.data, start, 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.
I believe @tustvold has resolved this in the latest version of this PR -- by making it clear that use_nulls
must be set to true
otherwise a panic
will result if the buffer is extended with nulls
let data = data.freeze(); | ||
|
||
assert_eq!(data.len(), 9); | ||
assert_eq!(data.null_buffer().unwrap().len(), 2); |
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.
2 because there are two bytes needed to hold the bitmap?
arrow/src/array/transform/mod.rs
Outdated
/// be computed only if `arrays` contains nulls. | ||
/// | ||
/// Code that plans to call [MutableArrayData::extend_nulls] MUST set `use_nulls` to `true`, | ||
/// in order to ensure that a null bitmap is computed. | ||
pub fn new(arrays: Vec<&'a ArrayData>, use_nulls: bool, capacity: usize) -> 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.
having thought some more about this, wouldn't something like compute_nulls
or create_null_bitmap
(instead of use_nulls
) be a better name, because:
(1) if it's true
, then a null bitmap is always created, no matter if any the input arrays have a null bitmap
(2) the documentation comment, I think, reads better as e.g.
if `compute_nulls` is `true` a null bitmap will be created regardless of the contents of `arrays`
@tustvold are you still seeing a 2x performance improvement in filter benchmarks after the latest changes? |
So it makes filtering arrays without nulls about takes ~50% less time, however, it does seem to make filtering arrays with nulls take 10% longer. This is likely down to the issue in #1229 , that the extend_bits function is ludicrously "hot" for these benchmarks where the runs are typically 1 or 2 elements long. I'd personally prefer to merge this as is and keep pushing forward, but I can also hold off on this until I've fixed #1229. |
Hmm I've changed my mind - will pause this until I've fixed #1229 as it will influence the benchmarks significantly |
Following #1229 this no longer yields a performance improvement, and for some reason seems to slow down null mask handling in the filter kernel (possibly related to the change to append_ranges) so going to close this |
Which issue does this PR close?
Closes #1224
Rationale for this change
See ticket.
This improves the performance of filtering arrays with no nulls by ~2x on my local machine, and will likely see similar performance improvements in other kernels.
What changes are included in this PR?
This changes MutableArrayData to skip building the null mask when it has already decided it is not needed
Are there any user-facing changes?
No