Skip to content

Commit 31e3f62

Browse files
committed
perf: scan all-valid run-end sums directly
Signed-off-by: Connor Tsui <connor.tsui20@gmail.com>
1 parent f6685e9 commit 31e3f62

6 files changed

Lines changed: 123 additions & 61 deletions

File tree

‎encodings/runend/benches/run_end_sum.rs‎

Lines changed: 28 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -150,20 +150,7 @@ fn sum_runend_fallback(bencher: Bencher, run_length: usize) {
150150

151151
#[divan::bench(args = [Validity::NonNullable, Validity::AllValid, Validity::AllInvalid])]
152152
fn sum_runend_validity(bencher: Bencher, validity: &Validity) {
153-
let ends = PrimitiveArray::from_iter((64..=LEN).step_by(64).map(|end| end as u64));
154-
let values = PrimitiveArray::new(
155-
(0..ends.len())
156-
.map(|index| i32::try_from(index).unwrap())
157-
.collect::<Buffer<_>>(),
158-
validity.clone(),
159-
);
160-
let array = RunEnd::try_new(
161-
ends.into_array(),
162-
values.into_array(),
163-
&mut SESSION.create_execution_ctx(),
164-
)
165-
.unwrap()
166-
.into_array();
153+
let array = runend_with_validity(validity);
167154

168155
bencher
169156
.with_inputs(|| SESSION.create_execution_ctx())
@@ -179,3 +166,30 @@ fn grouped_runend<const GROUP_SIZE: u32>(bencher: Bencher, run_length: usize) {
179166
fn grouped_runend_fallback<const GROUP_SIZE: u32>(bencher: Bencher, run_length: usize) {
180167
bench_grouped(bencher, runend(run_length), GROUP_SIZE, &FALLBACK_SESSION);
181168
}
169+
170+
fn runend_with_validity(validity: &Validity) -> ArrayRef {
171+
let ends = PrimitiveArray::from_iter((64..=LEN).step_by(64).map(|end| end as u64));
172+
let values = PrimitiveArray::new(
173+
(0..ends.len())
174+
.map(|index| i32::try_from(index).unwrap())
175+
.collect::<Buffer<_>>(),
176+
validity.clone(),
177+
);
178+
RunEnd::try_new(
179+
ends.into_array(),
180+
values.into_array(),
181+
&mut SESSION.create_execution_ctx(),
182+
)
183+
.unwrap()
184+
.into_array()
185+
}
186+
187+
#[divan::bench(consts = [2, 128])]
188+
fn grouped_runend_all_valid<const GROUP_SIZE: u32>(bencher: Bencher) {
189+
bench_grouped(
190+
bencher,
191+
runend_with_validity(&Validity::AllValid),
192+
GROUP_SIZE,
193+
&SESSION,
194+
);
195+
}

‎encodings/runend/src/compute/sum/grouped.rs‎

Lines changed: 29 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
1010
use std::ops::Range;
1111

12-
use itertools::Either;
1312
use vortex_array::ArrayRef;
1413
use vortex_array::ExecutionCtx;
1514
use vortex_array::IntoArray;
@@ -31,7 +30,6 @@ use vortex_array::validity::Validity;
3130
use vortex_buffer::BitBuffer;
3231
use vortex_buffer::BitBufferMut;
3332
use vortex_error::VortexResult;
34-
use vortex_mask::AllOr;
3533
use vortex_mask::Mask;
3634

3735
use super::RunEndInputs;
@@ -40,8 +38,9 @@ use super::empty_partial;
4038
use super::runs::add_float_run;
4139
use super::runs::add_signed_run;
4240
use super::runs::add_unsigned_run;
43-
use super::runs::sum_next_range;
44-
use super::runs::sum_range;
41+
use super::runs::sum_all_valid;
42+
use super::runs::sum_next_valid_range;
43+
use super::runs::sum_valid_range;
4544
use crate::RunEnd;
4645

4746
impl DynGroupedAggregateKernel for RunEndSumKernel {
@@ -78,21 +77,20 @@ impl DynGroupedAggregateKernel for RunEndSumKernel {
7877
};
7978

8079
let ranges = groups.group_ranges(ctx)?;
81-
let valid_runs = runs.validity.indices();
8280

8381
let (results, empty_groups) = match_each_unsigned_integer_ptype!(runs.ends.ptype(), |E| {
8482
let ends = runs.ends.as_slice::<E>();
8583
match_each_native_ptype!(runs.values.ptype(),
8684
unsigned: |T| {
87-
sum_groups(ends, runs.values.as_slice::<T>(), valid_runs, &ranges,
85+
sum_groups(ends, runs.values.as_slice::<T>(), &runs.validity, &ranges,
8886
&validity, runs.offset, add_unsigned_run)
8987
},
9088
signed: |T| {
91-
sum_groups(ends, runs.values.as_slice::<T>(), valid_runs, &ranges,
89+
sum_groups(ends, runs.values.as_slice::<T>(), &runs.validity, &ranges,
9290
&validity, runs.offset, add_signed_run)
9391
},
9492
floating: |T| {
95-
sum_groups(ends, runs.values.as_slice::<T>(), valid_runs, &ranges,
93+
sum_groups(ends, runs.values.as_slice::<T>(), &runs.validity, &ranges,
9694
&validity, runs.offset,
9795
|sum, value, len| add_float_run(sum, value, len, options.skip_nans))
9896
}
@@ -116,30 +114,38 @@ impl DynGroupedAggregateKernel for RunEndSumKernel {
116114
fn sum_groups<E: IntegerPType, T: NativePType, A: NativePType>(
117115
ends: &[E],
118116
values: &[T],
119-
validity: AllOr<&[usize]>,
117+
validity: &Mask,
120118
ranges: &GroupRanges,
121119
group_validity: &Mask,
122120
offset: usize,
123121
add_run: impl Fn(A, T, usize) -> Option<A>,
124122
) -> (PrimitiveArray, BitBuffer) {
125-
match ranges {
126-
// Consecutive groups reuse the cursor, including a run split across group boundaries.
127-
GroupRanges::FixedSizeList { .. } => {
128-
let indices = match validity {
129-
AllOr::All => Either::Left(0..ends.len()),
130-
AllOr::None => Either::Left(0..0),
131-
AllOr::Some(indices) => Either::Right(indices.iter().copied()),
132-
};
133-
let mut indices = indices.peekable();
134-
123+
match (validity, ranges) {
124+
(Mask::AllTrue(_), GroupRanges::FixedSizeList { .. }) => {
125+
let mut cursor = 0;
126+
collect_group_sums(ranges, group_validity, offset, |range| {
127+
sum_all_valid(ends, values, &mut cursor, range, &add_run)
128+
})
129+
}
130+
(Mask::AllTrue(_), GroupRanges::ListView { .. }) => {
131+
collect_group_sums(ranges, group_validity, offset, |range| {
132+
let mut cursor = ends.partition_point(|end| end.as_() <= range.start);
133+
sum_all_valid(ends, values, &mut cursor, range, &add_run)
134+
})
135+
}
136+
(Mask::AllFalse(_), _) => collect_group_sums(ranges, group_validity, offset, |_| {
137+
(Some(A::default()), true)
138+
}),
139+
(Mask::Values(validity), GroupRanges::FixedSizeList { .. }) => {
140+
let mut indices = validity.indices().iter().copied().peekable();
135141
collect_group_sums(ranges, group_validity, offset, |range| {
136-
sum_next_range(ends, values, &mut indices, range, &add_run)
142+
sum_next_valid_range(ends, values, &mut indices, range, &add_run)
137143
})
138144
}
139-
// These ranges can overlap or go backwards, so each group seeks independently.
140-
GroupRanges::ListView { .. } => {
145+
(Mask::Values(validity), GroupRanges::ListView { .. }) => {
146+
let indices = validity.indices();
141147
collect_group_sums(ranges, group_validity, offset, |range| {
142-
sum_range(ends, values, &validity, range, &add_run)
148+
sum_valid_range(ends, values, indices, range, &add_run)
143149
})
144150
}
145151
}

‎encodings/runend/src/compute/sum/mod.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
//!
66
//! Empty arrays and all-null inputs return before decoding the children. Otherwise, each valid
77
//! run contributes its value multiplied by the length included in the input.
8+
//! All-valid inputs scan the end and value slices directly. Only partially valid inputs use indices.
89
//!
910
//! Whole-array sums visit one range, clipped at the array's slice boundaries. Fixed-size groups
1011
//! share a forward cursor. List-view groups can overlap or arrive out of order, so each group

‎encodings/runend/src/compute/sum/runs.rs‎

Lines changed: 49 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,19 @@
33

44
//! Weighted reduction of the valid runs that intersect a logical range.
55
//!
6-
//! Seeking and forward traversal share one loop that clips each run to the requested range.
6+
//! All-valid inputs traverse the end and value slices directly. Partially valid inputs visit only
7+
//! their valid run indices. Both paths clip the boundary runs to the requested range.
78
//! Signed arithmetic widens the product, and floating-point arithmetic uses fused multiply-add,
89
//! so a run can cancel a preceding sum even when its product alone exceeds the result type.
910
1011
use std::iter::Peekable;
1112
use std::ops::Range;
1213

13-
use itertools::Either;
1414
use num_traits::AsPrimitive;
1515
use num_traits::ToPrimitive;
1616
use vortex_array::dtype::IntegerPType;
1717
use vortex_array::dtype::NativePType;
1818
use vortex_error::VortexExpect;
19-
use vortex_mask::AllOr;
2019

2120
pub(super) fn add_unsigned_run<T: AsPrimitive<u64>>(sum: u64, value: T, len: usize) -> Option<u64> {
2221
value
@@ -44,32 +43,66 @@ pub(super) fn add_float_run<T: NativePType>(
4443
Some(value.mul_add(len as f64, sum))
4544
}
4645

47-
/// Locate the first intersecting valid run before summing a range.
48-
pub(super) fn sum_range<E: IntegerPType, T: NativePType, A: NativePType>(
46+
/// Sum an all-valid range directly from the end and value slices.
47+
///
48+
/// The cursor is a position in the slices, retained for consecutive groups. Ranges must be ordered
49+
/// and non-overlapping. Arbitrary ranges must first position the cursor with a binary search.
50+
pub(super) fn sum_all_valid<E: IntegerPType, T: NativePType, A: NativePType>(
4951
ends: &[E],
5052
values: &[T],
51-
validity: &AllOr<&[usize]>,
53+
cursor: &mut usize,
5254
range: Range<usize>,
5355
add_run: impl Fn(A, T, usize) -> Option<A>,
5456
) -> (Option<A>, bool) {
55-
let first = ends.partition_point(|end| end.as_() <= range.start);
56-
let indices = match validity {
57-
AllOr::All => Either::Left(first..ends.len()),
58-
AllOr::None => return (Some(A::default()), true),
59-
AllOr::Some(indices) => {
60-
let start = indices.partition_point(|&index| index < first);
61-
Either::Right(indices[start..].iter().copied())
57+
let mut sum = A::default();
58+
if range.is_empty() {
59+
return (Some(sum), true);
60+
}
61+
62+
// Skipped null groups or an earlier overflow can leave the cursor behind this range.
63+
while ends[*cursor].as_() <= range.start {
64+
*cursor += 1;
65+
}
66+
67+
let mut start = range.start;
68+
for (&end, &value) in ends[*cursor..].iter().zip(&values[*cursor..]) {
69+
let end = end.as_();
70+
if end >= range.end {
71+
*cursor += usize::from(end == range.end);
72+
return (add_run(sum, value, range.end - start), false);
6273
}
63-
};
6474

65-
sum_next_range(ends, values, &mut indices.peekable(), range, add_run)
75+
*cursor += 1;
76+
let Some(next) = add_run(sum, value, end - start) else {
77+
return (None, false);
78+
};
79+
sum = next;
80+
start = end;
81+
}
82+
83+
(Some(sum), false)
84+
}
85+
86+
/// Locate the first intersecting valid run before summing an arbitrary range.
87+
pub(super) fn sum_valid_range<E: IntegerPType, T: NativePType, A: NativePType>(
88+
ends: &[E],
89+
values: &[T],
90+
indices: &[usize],
91+
range: Range<usize>,
92+
add_run: impl Fn(A, T, usize) -> Option<A>,
93+
) -> (Option<A>, bool) {
94+
let first = ends.partition_point(|end| end.as_() <= range.start);
95+
let start = indices.partition_point(|&index| index < first);
96+
let mut indices = indices[start..].iter().copied().peekable();
97+
98+
sum_next_valid_range(ends, values, &mut indices, range, add_run)
6699
}
67100

68101
/// Sum the next range while retaining a run that crosses its end.
69102
///
70103
/// The caller must supply non-overlapping ranges in increasing order. Skipped null groups and
71104
/// early overflow returns can leave the cursor behind the next range's start.
72-
pub(super) fn sum_next_range<E: IntegerPType, T: NativePType, A: NativePType>(
105+
pub(super) fn sum_next_valid_range<E: IntegerPType, T: NativePType, A: NativePType>(
73106
ends: &[E],
74107
values: &[T],
75108
indices: &mut Peekable<impl Iterator<Item = usize>>,

‎encodings/runend/src/compute/sum/tests.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,7 @@ fn grouped_sums(
306306
#[rstest]
307307
#[case::empty(0, 0, false, buffer![1i64, -2, 3, -4, 5].into_array())]
308308
#[case::short_groups(0, 2, false, buffer![1i64, -2, 3, -4, 5].into_array())]
309+
#[case::all_valid_with_null_groups(1, 8, true, buffer![1i64, -2, 3, -4, 5].into_array())]
309310
#[case::sliced_null_groups(1, 8, true,
310311
PrimitiveArray::from_option_iter([None, Some(2i64), None, Some(4), Some(5)]).into_array())]
311312
#[case::all_null(1, 8, true, PrimitiveArray::from_option_iter([None::<i64>; 5]).into_array())]

‎encodings/runend/src/compute/sum/whole.rs‎

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ use vortex_array::match_each_unsigned_integer_ptype;
2222
use vortex_array::scalar::PValue;
2323
use vortex_array::scalar::Scalar;
2424
use vortex_error::VortexResult;
25-
use vortex_mask::AllOr;
25+
use vortex_mask::Mask;
2626

2727
use super::RunEndInputs;
2828
use super::RunEndSumKernel;
@@ -31,7 +31,8 @@ use super::partial_scalar;
3131
use super::runs::add_float_run;
3232
use super::runs::add_signed_run;
3333
use super::runs::add_unsigned_run;
34-
use super::runs::sum_range;
34+
use super::runs::sum_all_valid;
35+
use super::runs::sum_valid_range;
3536
use crate::RunEnd;
3637

3738
impl DynAggregateKernel for RunEndSumKernel {
@@ -58,19 +59,18 @@ impl DynAggregateKernel for RunEndSumKernel {
5859
return Ok(Some(empty_partial(aggregate_fn, batch.dtype())?));
5960
};
6061
let range = runs.offset..runs.offset + batch.len();
61-
let valid_runs = runs.validity.indices();
6262

6363
let (sum, is_empty) = match_each_unsigned_integer_ptype!(runs.ends.ptype(), |E| {
6464
let ends = runs.ends.as_slice::<E>();
6565
match_each_native_ptype!(runs.values.ptype(),
6666
unsigned: |T| {
67-
sum_scalar(ends, runs.values.as_slice::<T>(), &valid_runs, range, add_unsigned_run)
67+
sum_scalar(ends, runs.values.as_slice::<T>(), &runs.validity, range, add_unsigned_run)
6868
},
6969
signed: |T| {
70-
sum_scalar(ends, runs.values.as_slice::<T>(), &valid_runs, range, add_signed_run)
70+
sum_scalar(ends, runs.values.as_slice::<T>(), &runs.validity, range, add_signed_run)
7171
},
7272
floating: |T| {
73-
sum_scalar(ends, runs.values.as_slice::<T>(), &valid_runs, range,
73+
sum_scalar(ends, runs.values.as_slice::<T>(), &runs.validity, range,
7474
|sum, value, len| add_float_run(sum, value, len, options.skip_nans))
7575
}
7676
)
@@ -83,11 +83,18 @@ impl DynAggregateKernel for RunEndSumKernel {
8383
fn sum_scalar<E: IntegerPType, T: NativePType, A: NativePType + Into<PValue>>(
8484
ends: &[E],
8585
values: &[T],
86-
validity: &AllOr<&[usize]>,
86+
validity: &Mask,
8787
range: Range<usize>,
8888
add_run: impl Fn(A, T, usize) -> Option<A>,
8989
) -> (Scalar, bool) {
90-
let (sum, is_empty) = sum_range(ends, values, validity, range, add_run);
90+
let (sum, is_empty) = match validity {
91+
Mask::AllTrue(_) => {
92+
let mut cursor = ends.partition_point(|end| end.as_() <= range.start);
93+
sum_all_valid(ends, values, &mut cursor, range, add_run)
94+
}
95+
Mask::AllFalse(_) => (Some(A::default()), true),
96+
Mask::Values(validity) => sum_valid_range(ends, values, validity.indices(), range, add_run),
97+
};
9198
let sum = match sum {
9299
Some(sum) => Scalar::primitive(sum, Nullable),
93100
None => Scalar::null(DType::Primitive(A::PTYPE, Nullable)),

0 commit comments

Comments
 (0)