perf: avoid re-allocation if buffer is not shared during BooleanArray::take_n_true - #10438
perf: avoid re-allocation if buffer is not shared during BooleanArray::take_n_true#10438Rich-T-kid wants to merge 5 commits into
BooleanArray::take_n_true#10438Conversation
| for i in end..len { | ||
| bit_util::unset_bit(mutable_buffer.as_slice_mut(), i); | ||
| } |
There was a problem hiding this comment.
Im sure there are faster ways to do this. I looked up a few ways to do this and im not too familar with bit operations so I left is as a basic loop for now
There was a problem hiding this comment.
See also a related PR
|
@alamb this is ready for review |
|
maybe we should do a microbenchmark to doublecheck this is a performance gain? we dont necessarily need to commit it, but just something to use as reference for reviewing this |
|
I'll make a seperate PR
makes sense to me, created this PR so we can run the benchmarks on it https://github.com/apache/arrow-rs/pull/10703/changes |
# 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. --> - works towards closing #10251. - related to #10438 # Rationale for this change see #10438 (comment) <!-- Why are you proposing this change? If this is already explained clearly in the issue then this section is not needed. Explaining clearly why changes are proposed helps reviewers understand your changes and offer better suggestions for fixes. --> # What changes are included in this PR? adds benchmarks for `BooleanArray::take_first` <!-- There is no need to duplicate the description in the issue here but it is sometimes worth providing a summary of the individual changes in this PR. --> # Are these changes tested? n/a <!-- We typically require tests for all PRs in order to: 1. Prevent the code from being accidentally broken by subsequent changes 2. Serve as another way to document the expected behavior of the code If tests are not included in your PR, please explain why (for example, are they covered by existing tests)? If this PR claims a performance improvement, please include evidence such as benchmark results. --> # Are there any user-facing changes? no <!-- If there are user-facing changes then we may require documentation to be updated before approving the PR. If there are any breaking changes to public APIs, please call them out. --> --------- Co-authored-by: rich-T-kid <richardbaah@MacBook-Air-de-Richard.local>
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as duplicate.
This comment was marked as duplicate.
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
BooleanArray::take_n_true
This comment was marked as duplicate.
This comment was marked as duplicate.
|
ooh wait the two benchmarks share the same name. Thats my mistake, fixed it here #10705 |
This comment was marked as outdated.
This comment was marked as outdated.
# 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. --> - context #10438 (comment) # Rationale for this change see #10438 (comment) <!-- Why are you proposing this change? If this is already explained clearly in the issue then this section is not needed. Explaining clearly why changes are proposed helps reviewers understand your changes and offer better suggestions for fixes. --> # What changes are included in this PR? update benchmark names <!-- There is no need to duplicate the description in the issue here but it is sometimes worth providing a summary of the individual changes in this PR. --> # Are these changes tested? n/a <!-- We typically require tests for all PRs in order to: 1. Prevent the code from being accidentally broken by subsequent changes 2. Serve as another way to document the expected behavior of the code If tests are not included in your PR, please explain why (for example, are they covered by existing tests)? If this PR claims a performance improvement, please include evidence such as benchmark results. --> # Are there any user-facing changes? n/a <!-- If there are user-facing changes then we may require documentation to be updated before approving the PR. If there are any breaking changes to public APIs, please call them out. -->
|
run benchmark boolean_array |
This comment was marked as duplicate.
This comment was marked as duplicate.
|
🤖 Arrow criterion benchmark completed (GKE) | trigger Instance: Comparing rich-T-kid/optimize-take-n-boolBuff (2e2815d) to 5ce0ebe (merge-base) diff Run configurationrun benchmark boolean_array
env:
BENCH_FILTER: "take_n_true"CPU Details (lscpu)Details
Resource Usagebase (merge-base)
branch
File an issue against this benchmark runner |
|
run benchmark boolean_array |
This comment was marked as duplicate.
This comment was marked as duplicate.
|
🤖 Arrow criterion benchmark completed (GKE) | trigger Instance: Comparing rich-T-kid/optimize-take-n-boolBuff (2e2815d) to 5ce0ebe (merge-base) diff Run configurationrun benchmark boolean_array
env:
BENCH_FILTER: "take_n_true"CPU Details (lscpu)Details
Resource Usagebase (merge-base)
branch
File an issue against this benchmark runner |
|
maybe we need to find a faster way to unset the bits? |
23d1576 to
27f06fb
Compare
|
how about something like this let mut_buffer_result = self.values.into_inner().into_mutable();
match mut_buffer_result {
Ok(mut mutable_buffer) => {
let raw_bytes = mutable_buffer.as_slice_mut();
let byte_idx_of_end = end / 8;
let bits_to_preserve = end % 8;
// end on a byte boundary, so just easily rewrite at byte level
if bits_to_preserve == 0 {
// TODO: this technically can modify bits beyond what the
// boolean buffer actually points to, but given we
// have unique ownership it should be fine? there could
// be pathological case where if this buffer was sliced
// there could be unused bytes that we still process,
// if we wanna bother with that edge case
raw_bytes
.iter_mut()
.skip(byte_idx_of_end)
.for_each(|b| *b = 0);
} else {
// if end in middle of a byte, need to unset only higher bits
raw_bytes[byte_idx_of_end] &= (1_u8 << bits_to_preserve) - 1;
raw_bytes
.iter_mut()
// +1 since we account for one byte above
.skip(byte_idx_of_end + 1)
.for_each(|b| *b = 0);
}
// TODO: this offset is wrong?
let boolean_buf = BooleanBuffer::new(mutable_buffer.into(), 0, len);
BooleanArray::new(boolean_buf, self.nulls)
}
Err(buf) => {
let mut builder = BooleanBufferBuilder::new(len);
builder.append_buffer(&BooleanBuffer::new(buf, 0, end));
builder.append_n(len - end, false);
BooleanArray::new(builder.finish(), self.nulls)
}
}essentially rewrite at the byte level, except for if the i did a single benchmark run and it seems promising, though i havent carefully checked for edge cases yet 🤔 |
Which issue does this PR close?
Buffer.into_mutableto reuse the allocation if possible intake_n_true#10251.Rationale for this change
If a boolean buffer is not shared we should re-use its allocation instead of building a new builder from scratch.
What changes are included in this PR?
match on
into_mutable, in the error case the old path is taken. If the buffer can be re-used we use it directly.Are these changes tested?
yes, existing test cover this behavior
Are there any user-facing changes?
no