Skip to content

Commit 8bfca37

Browse files
committed
Compare primitive values with RowFn
Signed-off-by: Connor Tsui <connor.tsui20@gmail.com>
1 parent b464dd5 commit 8bfca37

5 files changed

Lines changed: 74 additions & 171 deletions

File tree

vortex-array/benches/compare.rs

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,11 @@
33

44
//! Benchmarks for the binary comparison path, over every array kind it accepts.
55
//!
6-
//! The primitive cases carry `#[cpu_features]`, so they are measured on every walltime
7-
//! CPU-feature leg rather than in simulation. Each is written once and compiled differently
8-
//! per leg: today the primitive comparison path is a portable lane kernel, and how well it
9-
//! auto-vectorizes is decided by the build. That is the baseline a hand-written kernel
10-
//! selected through `cfg(target_feature)` has to beat, measured on the silicon it would run
11-
//! on.
6+
//! The primitive cases carry `#[cpu_features]`, so they are measured on every walltime CPU-feature
7+
//! leg rather than in simulation. They all exercise the same [`RowFn`] comparison path, including
8+
//! its runtime-selected packed Boolean collector.
9+
//!
10+
//! [`RowFn`]: vortex_array::scalar_fn::unstable::row::RowFn
1211
//!
1312
//! The boolean, decimal, string, and struct cases are not tagged. A wider vector register is
1413
//! not what decides them: booleans are already word-at-a-time over a bitmap, decimals are

vortex-array/src/scalar_fn/fns/binary/compare/mod.rs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@
44
//! Native comparison kernels.
55
//!
66
//! [`execute_compare`] dispatches on the logical [`DType`] of its operands and evaluates every
7-
//! comparison directly over Vortex canonical arrays — bit buffers for booleans, lane kernels from
8-
//! `vortex-compute` for primitives and decimals, binary views for strings/bytes, and a row-wise
9-
//! comparator for nested types. There is no Arrow fallback.
7+
//! comparison directly over Vortex canonical arrays: bit buffers for booleans, [`RowFn`] for
8+
//! primitives, lane kernels from `vortex-compute` for decimals, binary views for strings/bytes, and
9+
//! a row-wise comparator for nested types. There is no Arrow fallback.
10+
//!
11+
//! [`RowFn`]: crate::scalar_fn::unstable::row::RowFn
1012
//!
1113
//! Floating point values compare with Vortex's total ordering (`NaN` is the largest value,
1214
//! `-0.0 < +0.0`, and equality is bitwise), matching [`Scalar`] comparison semantics.
@@ -211,7 +213,7 @@ fn compare_arrays(
211213
)
212214
.into_array()),
213215
DType::Bool(_) => boolean::compare_bool(lhs, rhs, op, nullability, ctx),
214-
DType::Primitive(..) => primitive::compare_primitive(lhs, rhs, op, nullability, ctx),
216+
DType::Primitive(..) => primitive::compare_primitive(lhs, rhs, op, ctx),
215217
DType::Decimal(..) => decimal::compare_decimal(lhs, rhs, op, nullability, ctx),
216218
DType::Utf8(_) | DType::Binary(_) => bytes::compare_bytes(lhs, rhs, op, nullability, ctx),
217219
DType::Struct(..) | DType::List(..) | DType::FixedSizeList(..) | DType::Map(..) => {
Lines changed: 63 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,28 @@
11
// SPDX-License-Identifier: Apache-2.0
22
// SPDX-FileCopyrightText: Copyright the Vortex contributors
33

4-
//! Native comparison of primitive arrays via bit-packing lane kernels.
4+
//! Primitive comparison execution through [`RowFn`].
5+
//!
6+
//! [`PrimitiveCompare`] delegates decoding, constant handling, validity, and packed Boolean output
7+
//! to the row executor. Its row kernel contains only the comparison selected by [`CompareOperator`].
58
6-
use vortex_buffer::BitBuffer;
79
use vortex_error::VortexResult;
810
use vortex_error::vortex_bail;
911

1012
use crate::ArrayRef;
1113
use crate::ExecutionCtx;
12-
use crate::IntoArray;
13-
use crate::arrays::BoolArray;
14-
use crate::arrays::ConstantArray;
1514
use crate::dtype::DType;
1615
use crate::dtype::NativePType;
17-
use crate::dtype::Nullability;
1816
use crate::dtype::PType;
1917
use crate::match_each_native_ptype;
20-
use crate::scalar::Scalar;
21-
use crate::scalar_fn::fns::binary::compare::collect_bits;
22-
use crate::scalar_fn::fns::binary::compare::collect_zip_bits;
23-
use crate::scalar_fn::fns::binary::compare::compare_validity;
24-
use crate::scalar_fn::fns::binary::primitive_operand::PrimitiveOperand;
18+
use crate::scalar_fn::ScalarFnId;
19+
use crate::scalar_fn::ScalarFnVTable;
20+
use crate::scalar_fn::VecExecutionArgs;
21+
use crate::scalar_fn::fns::binary::Binary;
2522
use crate::scalar_fn::fns::operators::CompareOperator;
23+
use crate::scalar_fn::unstable::row::RowFn;
24+
use crate::scalar_fn::unstable::row::RowVisitor;
25+
use crate::scalar_fn::unstable::row::execute_rows;
2626

2727
/// Compare two primitive arrays of the same [`PType`].
2828
///
@@ -32,99 +32,71 @@ pub(super) fn compare_primitive(
3232
lhs: &ArrayRef,
3333
rhs: &ArrayRef,
3434
op: CompareOperator,
35-
nullability: Nullability,
3635
ctx: &mut ExecutionCtx,
3736
) -> VortexResult<ArrayRef> {
38-
let ptype = PType::try_from(lhs.dtype())?;
39-
match_each_native_ptype!(ptype, |T| {
40-
compare_primitive_typed::<T>(lhs, rhs, op, nullability, ctx)
41-
})
37+
let args = VecExecutionArgs::new(vec![lhs.clone(), rhs.clone()], lhs.len());
38+
39+
execute_rows(&PrimitiveCompare, &op, &args, ctx)
4240
}
4341

44-
fn compare_primitive_typed<T: NativePType>(
45-
lhs: &ArrayRef,
46-
rhs: &ArrayRef,
47-
op: CompareOperator,
48-
nullability: Nullability,
49-
ctx: &mut ExecutionCtx,
50-
) -> VortexResult<ArrayRef> {
51-
let len = lhs.len();
52-
let lhs = PrimitiveOperand::<T>::try_new(lhs, ctx)?;
53-
let rhs = PrimitiveOperand::<T>::try_new(rhs, ctx)?;
54-
if lhs.len() != rhs.len() {
55-
vortex_bail!(
56-
"compare operator requires equal lengths, got {} and {}",
57-
lhs.len(),
58-
rhs.len()
59-
);
60-
}
42+
/// Internal row execution for primitive comparison operators.
43+
#[derive(Clone)]
44+
struct PrimitiveCompare;
6145

62-
let validity = compare_validity(lhs.validity(), rhs.validity(), nullability)?;
46+
impl RowFn for PrimitiveCompare {
47+
type Options = CompareOperator;
6348

64-
let bits = match (&lhs, &rhs) {
65-
(
66-
PrimitiveOperand::Array { values: lhs, .. },
67-
PrimitiveOperand::Array { values: rhs, .. },
68-
) => compare_slices(lhs, rhs, op),
69-
(
70-
PrimitiveOperand::Array { values: lhs, .. },
71-
PrimitiveOperand::Constant { value: rhs, .. },
72-
) => compare_slice_constant(lhs, *rhs, op),
73-
(
74-
PrimitiveOperand::Constant { value: lhs, .. },
75-
PrimitiveOperand::Array { values: rhs, .. },
76-
) => compare_slice_constant(rhs, *lhs, op.swap()),
77-
(
78-
PrimitiveOperand::Constant { value: lhs, .. },
79-
PrimitiveOperand::Constant { value: rhs, .. },
80-
) => {
81-
// Unreachable through `execute_compare` (constant-constant is folded there), but
82-
// cheap to answer anyway.
83-
BitBuffer::full(apply_op(*lhs, *rhs, op), len)
84-
}
85-
(PrimitiveOperand::Null(_), _) | (_, PrimitiveOperand::Null(_)) => {
86-
return Ok(
87-
ConstantArray::new(Scalar::null(DType::Bool(Nullability::Nullable)), len)
88-
.into_array(),
89-
);
90-
}
91-
};
49+
const ARG_NAMES: &'static [&'static str] = &["lhs", "rhs"];
9250

93-
Ok(BoolArray::try_new(bits, validity)?.into_array())
94-
}
51+
const INFALLIBLE: bool = true;
9552

96-
#[inline(always)]
97-
fn apply_op<T: NativePType>(lhs: T, rhs: T, op: CompareOperator) -> bool {
98-
match op {
99-
CompareOperator::Eq => lhs.is_eq(rhs),
100-
CompareOperator::NotEq => !lhs.is_eq(rhs),
101-
CompareOperator::Gt => lhs.is_gt(rhs),
102-
CompareOperator::Gte => lhs.is_ge(rhs),
103-
CompareOperator::Lt => lhs.is_lt(rhs),
104-
CompareOperator::Lte => lhs.is_le(rhs),
53+
fn id(&self) -> ScalarFnId {
54+
// `PrimitiveCompare` is a private implementation detail of `Binary`: it is never registered
55+
// or serialized independently. Reusing the public ID keeps execution errors attributed to
56+
// `Binary`. If this type becomes registrable, it needs its own ID and persistence contract.
57+
ScalarFnVTable::id(&Binary)
10558
}
106-
}
10759

108-
fn compare_slices<T: NativePType>(lhs: &[T], rhs: &[T], op: CompareOperator) -> BitBuffer {
109-
// Dispatch the operator outside the lane loop so each instantiation vectorizes a single
110-
// branch-free predicate.
111-
match op {
112-
CompareOperator::Eq => collect_zip_bits(lhs, rhs, |a: T, b: T| a.is_eq(b)),
113-
CompareOperator::NotEq => collect_zip_bits(lhs, rhs, |a: T, b: T| !a.is_eq(b)),
114-
CompareOperator::Gt => collect_zip_bits(lhs, rhs, T::is_gt),
115-
CompareOperator::Gte => collect_zip_bits(lhs, rhs, T::is_ge),
116-
CompareOperator::Lt => collect_zip_bits(lhs, rhs, T::is_lt),
117-
CompareOperator::Lte => collect_zip_bits(lhs, rhs, T::is_le),
60+
fn dispatch<V: RowVisitor>(
61+
&self,
62+
op: &Self::Options,
63+
args: &[DType],
64+
visitor: V,
65+
) -> VortexResult<V::VisitResult> {
66+
let [lhs_dtype, _] = args else {
67+
vortex_bail!(
68+
"a primitive comparison requires two operands, got {}",
69+
args.len(),
70+
);
71+
};
72+
let ptype = PType::try_from(lhs_dtype)?;
73+
74+
match_each_native_ptype!(ptype, |T| { visit_compare::<T, V>(*op, visitor) })
11875
}
11976
}
12077

121-
fn compare_slice_constant<T: NativePType>(lhs: &[T], rhs: T, op: CompareOperator) -> BitBuffer {
78+
fn visit_compare<T, V>(op: CompareOperator, visitor: V) -> VortexResult<V::VisitResult>
79+
where
80+
T: NativePType,
81+
V: RowVisitor,
82+
{
12283
match op {
123-
CompareOperator::Eq => collect_bits(lhs, |a: T| a.is_eq(rhs)),
124-
CompareOperator::NotEq => collect_bits(lhs, |a: T| !a.is_eq(rhs)),
125-
CompareOperator::Gt => collect_bits(lhs, |a: T| a.is_gt(rhs)),
126-
CompareOperator::Gte => collect_bits(lhs, |a: T| a.is_ge(rhs)),
127-
CompareOperator::Lt => collect_bits(lhs, |a: T| a.is_lt(rhs)),
128-
CompareOperator::Lte => collect_bits(lhs, |a: T| a.is_le(rhs)),
84+
CompareOperator::Eq => visit_compare_with::<T, V>(visitor, T::is_eq),
85+
CompareOperator::NotEq => visit_compare_with::<T, V>(visitor, |lhs, rhs| !lhs.is_eq(rhs)),
86+
CompareOperator::Gt => visit_compare_with::<T, V>(visitor, T::is_gt),
87+
CompareOperator::Gte => visit_compare_with::<T, V>(visitor, T::is_ge),
88+
CompareOperator::Lt => visit_compare_with::<T, V>(visitor, T::is_lt),
89+
CompareOperator::Lte => visit_compare_with::<T, V>(visitor, T::is_le),
12990
}
13091
}
92+
93+
fn visit_compare_with<T, V>(
94+
visitor: V,
95+
compare: impl Fn(T, T) -> bool,
96+
) -> VortexResult<V::VisitResult>
97+
where
98+
T: NativePType,
99+
V: RowVisitor,
100+
{
101+
visitor.visit_bool::<(T, T), true>(move |(lhs, rhs)| compare(lhs, rhs))
102+
}

vortex-array/src/scalar_fn/fns/binary/mod.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,6 @@ mod compare;
4545
pub use compare::*;
4646
mod numeric;
4747
pub(crate) use numeric::*;
48-
mod primitive_operand;
4948

5049
use crate::scalar::NumericOperator;
5150
use crate::scalar::Scalar;

vortex-array/src/scalar_fn/fns/binary/primitive_operand.rs

Lines changed: 0 additions & 69 deletions
This file was deleted.

0 commit comments

Comments
 (0)