hhhizzz commented on code in PR #10446:
URL: https://github.com/apache/arrow-rs/pull/10446#discussion_r3718326685


##########
parquet/src/arrow/arrow_reader/selection/algebra.rs:
##########
@@ -269,39 +269,77 @@ pub(super) fn union_row_selections(left: &[RowSelector], 
right: &[RowSelector])
 /// Bitwise AND of two mask-backed selections. Longer side's tail passes 
through.
 pub(super) fn intersect_masks(l: &BooleanBuffer, r: &BooleanBuffer) -> 
BooleanBuffer {
     if l.len() == r.len() {
-        return l & r;
-    }
-    let common = l.len().min(r.len());
-    let head = &l.slice(0, common) & &r.slice(0, common);
-    let (longer, longer_len) = if l.len() > r.len() {
-        (l, l.len())
-    } else {
-        (r, r.len())
-    };
-    let tail = longer.slice(common, longer_len - common);
-    let mut builder = BooleanBufferBuilder::new(longer_len);
-    builder.append_buffer(&head);
-    builder.append_buffer(&tail);
-    builder.finish()
+        return combine_equal_masks(l, r, |a, b| a & b);
+    }
+    combine_uneven_masks(l, r, |a, b| a & b)
 }
 
 /// Bitwise OR of two mask-backed selections. Longer side's tail passes 
through.
 pub(super) fn union_masks(l: &BooleanBuffer, r: &BooleanBuffer) -> 
BooleanBuffer {
     if l.len() == r.len() {
-        return l | r;
-    }
-    let common = l.len().min(r.len());
-    let head = &l.slice(0, common) | &r.slice(0, common);
-    let (longer, longer_len) = if l.len() > r.len() {
-        (l, l.len())
-    } else {
-        (r, r.len())
-    };
-    let tail = longer.slice(common, longer_len - common);
-    let mut builder = BooleanBufferBuilder::new(longer_len);
-    builder.append_buffer(&head);
-    builder.append_buffer(&tail);
-    builder.finish()
+        return combine_equal_masks(l, r, |a, b| a | b);

Review Comment:
   The logical semantics are unchanged, but I think we still have “user-facing 
changes” here.
   
   `RowSelection::as_mask()` is public, so callers can observe that 
equal-length intersection/union previously returned an offset-zero 
`BooleanBuffer`, while this implementation may preserve a non-zero offset. This 
also changes the observable `values()`, `inner()`, and `ptr_eq()` 
representation.
   
   I don't think this is a breaking API change because `as_mask()` does not 
promise normalization and consumers must already honor 
`BooleanBuffer::offset()`. Could we clarify the PR wording to say that there 
are no public signature or logical-selection changes, while the underlying mask 
layout may differ?
   



##########
parquet/benches/row_selector.rs:
##########
@@ -26,6 +26,21 @@ use std::hint;
 /// [`RowSelector`]s per row, so the RLE encoding dominates.
 const MASK_RUN_LENGTHS: &[usize] = &[1, 4, 16, 32, 48, 64, 96, 128];
 
+const MASK_ALGEBRA_ROWS: usize = 3_000_000;
+
+/// Operand length pairs. Unequal lengths pass the longer side's tail through 
unchanged,
+/// so the ratio decides how much of the work is the bitwise combine versus 
the tail.
+const MASK_ALGEBRA_LENGTHS: &[(&str, usize, usize)] = &[
+    ("equal", MASK_ALGEBRA_ROWS, MASK_ALGEBRA_ROWS),
+    ("tail1", MASK_ALGEBRA_ROWS, MASK_ALGEBRA_ROWS - 1),
+    ("tail1of3", MASK_ALGEBRA_ROWS, MASK_ALGEBRA_ROWS * 2 / 3),
+    ("tail_most", MASK_ALGEBRA_ROWS, 1_000),
+];
+
+/// Bit offsets applied to both operands. Masks come from 
[`BooleanBuffer::slice`], so a
+/// non-zero offset is normal, and one that is not byte aligned is the 
expensive case.
+const MASK_ALGEBRA_OFFSETS: &[(&str, usize)] = &[("aligned", 0), ("unaligned", 
3)];

Review Comment:
   Could we include more case where the operands have different relative 
offsets?
   
   At the moment both operands use the same offset, so the unaligned case only 
exercises the `left_offset % 64 == right_offset % 64` fast path in 
`BooleanBuffer::from_bitwise_binary_op`. When the offsets differ, for example 
`(3, 5)`, the helper takes the shifting fallback and returns a zero-offset 
buffer, so the old implementation does not necessarily pay the extra 
normalization copy described in the PR.
   
   Something like `(0, 0)`, `(3, 3)`, `(3, 5)`, and `(3, 67)` would make the 
benchmark coverage and the performance claim more precise. Alternatively, the 
PR description could narrow “non-byte-aligned operands” to “operands sharing a 
non-zero sub-64-bit alignment”.



##########
parquet/src/arrow/arrow_reader/selection/algebra.rs:
##########
@@ -269,39 +269,77 @@ pub(super) fn union_row_selections(left: &[RowSelector], 
right: &[RowSelector])
 /// Bitwise AND of two mask-backed selections. Longer side's tail passes 
through.
 pub(super) fn intersect_masks(l: &BooleanBuffer, r: &BooleanBuffer) -> 
BooleanBuffer {
     if l.len() == r.len() {
-        return l & r;
-    }
-    let common = l.len().min(r.len());
-    let head = &l.slice(0, common) & &r.slice(0, common);
-    let (longer, longer_len) = if l.len() > r.len() {
-        (l, l.len())
-    } else {
-        (r, r.len())
-    };
-    let tail = longer.slice(common, longer_len - common);
-    let mut builder = BooleanBufferBuilder::new(longer_len);
-    builder.append_buffer(&head);
-    builder.append_buffer(&tail);
-    builder.finish()
+        return combine_equal_masks(l, r, |a, b| a & b);
+    }
+    combine_uneven_masks(l, r, |a, b| a & b)
 }
 
 /// Bitwise OR of two mask-backed selections. Longer side's tail passes 
through.
 pub(super) fn union_masks(l: &BooleanBuffer, r: &BooleanBuffer) -> 
BooleanBuffer {
     if l.len() == r.len() {
-        return l | r;
-    }
-    let common = l.len().min(r.len());
-    let head = &l.slice(0, common) | &r.slice(0, common);
-    let (longer, longer_len) = if l.len() > r.len() {
-        (l, l.len())
-    } else {
-        (r, r.len())
-    };
-    let tail = longer.slice(common, longer_len - common);
-    let mut builder = BooleanBufferBuilder::new(longer_len);
-    builder.append_buffer(&head);
-    builder.append_buffer(&tail);
-    builder.finish()
+        return combine_equal_masks(l, r, |a, b| a | b);
+    }
+    combine_uneven_masks(l, r, |a, b| a | b)
+}
+
+/// Combines two masks of equal length with the bitwise operation `op`.
+///
+/// `BitAnd`/`BitOr` on `&BooleanBuffer` normalise the result to a zero bit 
offset,
+/// which costs a second allocation and a shifting copy of the whole mask when 
the
+/// operands are not byte aligned. Building the buffer directly keeps the 
offset,
+/// as the uneven path does.
+fn combine_equal_masks<F>(l: &BooleanBuffer, r: &BooleanBuffer, op: F) -> 
BooleanBuffer
+where
+    F: FnMut(u64, u64) -> u64,
+{
+    BooleanBuffer::from_bitwise_binary_op(
+        l.values(),
+        l.offset(),
+        r.values(),
+        r.offset(),
+        l.len(),
+        op,
+    )
+}
+
+/// Combines two masks of differing lengths with the bitwise operation `op`,
+/// passing the longer side's tail through unchanged.
+///
+/// The longer mask is copied once into a [`MutableBuffer`] and `op` is then
+/// applied in place over the common prefix. This avoids materialising the
+/// prefix into its own buffer and copying both prefix and tail again through a
+/// [`BooleanBufferBuilder`].
+///
+/// Neither the mask offsets nor the prefix length are assumed to be byte
+/// aligned: the copy keeps the longer mask's sub-byte offset so it stays a
+/// plain byte copy, and the offset is carried over to the returned buffer.
+fn combine_uneven_masks<F>(l: &BooleanBuffer, r: &BooleanBuffer, op: F) -> 
BooleanBuffer

Review Comment:
   Nit: a few names could make the offset arithmetic easier to follow:
   
   - `combine_uneven_masks` -> `combine_unequal_length_masks`
   - `bit_offset` -> `offset_within_byte` or `sub_byte_offset`
   - `start` / `end` -> `start_byte` / `end_byte`
   
   In particular, `bit_offset` currently looks like the full 
`BooleanBuffer::offset()`, but it is specifically `longer.offset() % 8`, which 
is an important distinction in this code.



##########
parquet/src/arrow/arrow_reader/selection/algebra.rs:
##########
@@ -269,39 +269,77 @@ pub(super) fn union_row_selections(left: &[RowSelector], 
right: &[RowSelector])
 /// Bitwise AND of two mask-backed selections. Longer side's tail passes 
through.
 pub(super) fn intersect_masks(l: &BooleanBuffer, r: &BooleanBuffer) -> 
BooleanBuffer {
     if l.len() == r.len() {
-        return l & r;
-    }
-    let common = l.len().min(r.len());
-    let head = &l.slice(0, common) & &r.slice(0, common);
-    let (longer, longer_len) = if l.len() > r.len() {
-        (l, l.len())
-    } else {
-        (r, r.len())
-    };
-    let tail = longer.slice(common, longer_len - common);
-    let mut builder = BooleanBufferBuilder::new(longer_len);
-    builder.append_buffer(&head);
-    builder.append_buffer(&tail);
-    builder.finish()
+        return combine_equal_masks(l, r, |a, b| a & b);
+    }
+    combine_uneven_masks(l, r, |a, b| a & b)
 }
 
 /// Bitwise OR of two mask-backed selections. Longer side's tail passes 
through.
 pub(super) fn union_masks(l: &BooleanBuffer, r: &BooleanBuffer) -> 
BooleanBuffer {
     if l.len() == r.len() {
-        return l | r;
-    }
-    let common = l.len().min(r.len());
-    let head = &l.slice(0, common) | &r.slice(0, common);
-    let (longer, longer_len) = if l.len() > r.len() {
-        (l, l.len())
-    } else {
-        (r, r.len())
-    };
-    let tail = longer.slice(common, longer_len - common);
-    let mut builder = BooleanBufferBuilder::new(longer_len);
-    builder.append_buffer(&head);
-    builder.append_buffer(&tail);
-    builder.finish()
+        return combine_equal_masks(l, r, |a, b| a | b);
+    }
+    combine_uneven_masks(l, r, |a, b| a | b)
+}
+
+/// Combines two masks of equal length with the bitwise operation `op`.
+///
+/// `BitAnd`/`BitOr` on `&BooleanBuffer` normalise the result to a zero bit 
offset,
+/// which costs a second allocation and a shifting copy of the whole mask when 
the
+/// operands are not byte aligned. Building the buffer directly keeps the 
offset,
+/// as the uneven path does.
+fn combine_equal_masks<F>(l: &BooleanBuffer, r: &BooleanBuffer, op: F) -> 
BooleanBuffer
+where
+    F: FnMut(u64, u64) -> u64,
+{
+    BooleanBuffer::from_bitwise_binary_op(
+        l.values(),
+        l.offset(),
+        r.values(),
+        r.offset(),
+        l.len(),
+        op,
+    )
+}
+
+/// Combines two masks of differing lengths with the bitwise operation `op`,
+/// passing the longer side's tail through unchanged.
+///
+/// The longer mask is copied once into a [`MutableBuffer`] and `op` is then
+/// applied in place over the common prefix. This avoids materialising the
+/// prefix into its own buffer and copying both prefix and tail again through a
+/// [`BooleanBufferBuilder`].
+///
+/// Neither the mask offsets nor the prefix length are assumed to be byte
+/// aligned: the copy keeps the longer mask's sub-byte offset so it stays a
+/// plain byte copy, and the offset is carried over to the returned buffer.
+fn combine_uneven_masks<F>(l: &BooleanBuffer, r: &BooleanBuffer, op: F) -> 
BooleanBuffer
+where
+    F: FnMut(u64, u64) -> u64,
+{
+    let (longer, shorter) = if l.len() > r.len() { (l, r) } else { (r, l) };
+    let common = shorter.len();
+    if common == 0 {

Review Comment:
   Is retaining the original backing allocation intentional for the 
empty-common-prefix case?
   
   `BooleanBuffer::clone()` is shallow, so if `longer` is a small slice of a 
much larger buffer, the result will continue to retain the entire original 
allocation. The previous builder path produced a compact copy of the logical 
mask.
   
   Avoiding the copy is attractive, but this changes the memory-retention 
behavior and could be surprising when the result outlives the input selection. 
Could we either document this tradeoff, add a test covering a small slice of a 
large buffer, or copy only the logical byte range here?



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to