Skip to content

Commit a169338

Browse files
authored
Optimise take on dictionaries with constant codes (#10643)
If the codes are constant we can just forward the value from the dictionary as a constant
1 parent 3cc1a5f commit a169338

6 files changed

Lines changed: 197 additions & 22 deletions

File tree

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
// SPDX-FileCopyrightText: Copyright the Vortex contributors
3+
4+
use vortex_error::VortexResult;
5+
use vortex_mask::Mask;
6+
7+
use super::slice::inherit_selection_stats;
8+
use super::slice::slice_constant_code;
9+
use crate::ArrayRef;
10+
use crate::IntoArray;
11+
use crate::array::ArrayView;
12+
use crate::arrays::Constant;
13+
use crate::arrays::Dict;
14+
use crate::arrays::DictArray;
15+
use crate::arrays::dict::DictArraySlotsExt;
16+
use crate::arrays::filter::FilterReduce;
17+
18+
impl FilterReduce for Dict {
19+
fn filter(array: ArrayView<'_, Dict>, mask: &Mask) -> VortexResult<Option<ArrayRef>> {
20+
if let Some(code) = array.codes().as_opt::<Constant>() {
21+
return slice_constant_code(array, code.scalar(), mask.true_count());
22+
}
23+
24+
let codes = array.codes().filter(mask.clone())?;
25+
if let Some(code) = codes.as_opt::<Constant>() {
26+
return slice_constant_code(array, code.scalar(), codes.len());
27+
}
28+
inherit_selection_stats(array.codes(), &codes);
29+
30+
// SAFETY: Selection preserves the integer code type and non-null code bounds, so every code
31+
// indexes the unchanged values. `new_unchecked` resets `all_values_referenced` because
32+
// selection can remove a value's last reference.
33+
let filtered = unsafe { DictArray::new_unchecked(codes, array.values().clone()) };
34+
35+
Ok(Some(filtered.into_array()))
36+
}
37+
}
38+
39+
#[cfg(test)]
40+
mod tests {
41+
use vortex_buffer::buffer;
42+
use vortex_error::VortexResult;
43+
use vortex_mask::Mask;
44+
45+
use crate::IntoArray;
46+
use crate::VortexSessionExecute;
47+
use crate::array_session;
48+
use crate::arrays::ConstantArray;
49+
use crate::arrays::Dict;
50+
use crate::arrays::DictArray;
51+
use crate::arrays::PrimitiveArray;
52+
use crate::arrays::dict::DictArraySlotsExt;
53+
use crate::assert_arrays_eq;
54+
use crate::expr::stats::Precision;
55+
use crate::expr::stats::Stat;
56+
use crate::expr::stats::StatsProviderExt;
57+
use crate::scalar::ScalarValue;
58+
59+
#[test]
60+
fn filter_constant_valid_code_keeps_only_referenced_value() -> VortexResult<()> {
61+
let mut ctx = array_session().create_execution_ctx();
62+
let dict = DictArray::new(
63+
ConstantArray::new(1u8, 5).into_array(),
64+
buffer![10i32, 20, 30].into_array(),
65+
);
66+
let filtered = dict.filter(Mask::from_iter([true, false, true, true, false]))?;
67+
68+
let filtered_dict = filtered.as_::<Dict>();
69+
assert_arrays_eq!(
70+
filtered_dict.values(),
71+
buffer![20i32].into_array(),
72+
&mut ctx
73+
);
74+
assert_arrays_eq!(
75+
filtered,
76+
PrimitiveArray::from_iter([20i32, 20, 20]),
77+
&mut ctx
78+
);
79+
Ok(())
80+
}
81+
82+
#[test]
83+
fn filter_inherits_true_order_stats_from_codes() -> VortexResult<()> {
84+
let codes = buffer![0u8, 0, 1, 2].into_array();
85+
codes
86+
.statistics()
87+
.set(Stat::IsSorted, Precision::exact(ScalarValue::Bool(true)));
88+
codes
89+
.statistics()
90+
.set(Stat::IsConstant, Precision::exact(ScalarValue::Bool(false)));
91+
let dict = DictArray::new(codes, buffer![10i32, 20, 30].into_array());
92+
93+
let filtered = dict.filter(Mask::from_iter([true, false, true, true]))?;
94+
95+
let filtered_codes = filtered.as_::<Dict>().codes().clone();
96+
assert_eq!(
97+
filtered_codes.statistics().get_as::<bool>(Stat::IsSorted),
98+
Precision::exact(true)
99+
);
100+
assert_eq!(
101+
filtered_codes.statistics().get_as::<bool>(Stat::IsConstant),
102+
Precision::Absent
103+
);
104+
Ok(())
105+
}
106+
}

‎vortex-array/src/arrays/dict/compute/mod.rs‎

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
mod cast;
55
mod compare;
66
mod fill_null;
7+
mod filter;
78
pub(crate) mod is_constant;
89
pub(crate) mod is_sorted;
910
mod like;
@@ -13,7 +14,6 @@ pub(crate) mod rules;
1314
mod slice;
1415

1516
use vortex_error::VortexResult;
16-
use vortex_mask::Mask;
1717

1818
use super::Dict;
1919
use super::DictArray;
@@ -23,7 +23,6 @@ use crate::ExecutionCtx;
2323
use crate::IntoArray;
2424
use crate::array::ArrayView;
2525
use crate::arrays::dict::DictArraySlotsExt;
26-
use crate::arrays::filter::FilterReduce;
2726

2827
impl TakeExecute for Dict {
2928
fn take(
@@ -42,19 +41,6 @@ impl TakeExecute for Dict {
4241
}
4342
}
4443

45-
impl FilterReduce for Dict {
46-
fn filter(array: ArrayView<'_, Dict>, mask: &Mask) -> VortexResult<Option<ArrayRef>> {
47-
let codes = array.codes().filter(mask.clone())?;
48-
49-
// SAFETY: Selection preserves the integer code type and non-null code bounds, so every code
50-
// indexes the unchanged values. `new_unchecked` resets `all_values_referenced` because
51-
// selection can remove a value's last reference.
52-
let filtered = unsafe { DictArray::new_unchecked(codes, array.values().clone()) };
53-
54-
Ok(Some(filtered.into_array()))
55-
}
56-
}
57-
5844
#[cfg(test)]
5945
mod test {
6046
use std::sync::LazyLock;

‎vortex-array/src/arrays/dict/compute/slice.rs‎

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ impl SliceReduce for Dict {
3131
let sliced_code = <Primitive as SliceReduce>::slice(codes, range)?
3232
.vortex_expect("Primitive SliceReduce should always return Some");
3333
// Because we specialize the primitive branch here, we have to make sure to handle the stat inheritance
34-
inherit_slice_stats(array.codes(), &sliced_code);
34+
inherit_selection_stats(array.codes(), &sliced_code);
3535
sliced_code
3636
} else {
3737
array.codes().slice(range)?
@@ -49,15 +49,17 @@ impl SliceReduce for Dict {
4949
}
5050
}
5151

52-
fn inherit_slice_stats(source: &ArrayRef, sliced: &ArrayRef) {
52+
/// Copies the stats of `source` that hold for any order-preserving selection of its rows, such as a
53+
/// slice or a filter.
54+
pub(super) fn inherit_selection_stats(source: &ArrayRef, selected: &ArrayRef) {
5355
source.statistics().with_iter(|iter| {
54-
sliced
56+
selected
5557
.statistics()
56-
.inherit(iter.filter(|(stat, value)| is_inheritable_true_slice_stat(*stat, value)));
58+
.inherit(iter.filter(|(stat, value)| is_inheritable_selection_stat(*stat, value)));
5759
});
5860
}
5961

60-
fn is_inheritable_true_slice_stat(stat: Stat, value: &Precision<ScalarValue>) -> bool {
62+
fn is_inheritable_selection_stat(stat: Stat, value: &Precision<ScalarValue>) -> bool {
6163
matches!(
6264
stat,
6365
Stat::IsConstant | Stat::IsSorted | Stat::IsStrictSorted
@@ -67,7 +69,7 @@ fn is_inheritable_true_slice_stat(stat: Stat, value: &Precision<ScalarValue>) ->
6769
.is_some_and(|value| matches!(value, ScalarValue::Bool(true)))
6870
}
6971

70-
fn slice_constant_code(
72+
pub(super) fn slice_constant_code(
7173
array: ArrayView<'_, Dict>,
7274
code: &Scalar,
7375
len: usize,

‎vortex-array/src/arrays/dict/take.rs‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ use crate::ExecutionCtx;
1111
use crate::IntoArray;
1212
use crate::array::ArrayView;
1313
use crate::array::VTable;
14+
use crate::arrays::Constant;
1415
use crate::arrays::ConstantArray;
1516
use crate::arrays::dict::DictArraySlotsExt;
1617
use crate::expr::stats::Precision;
@@ -170,3 +171,35 @@ pub(crate) fn propagate_take_stats(
170171
)
171172
})
172173
}
174+
175+
/// Executes a dictionary whose codes are constant by expanding the one value they select, rather
176+
/// than materializing the codes and gathering through them.
177+
///
178+
/// The executor offers the codes child to its parent's kernels before the values child, so this
179+
/// runs ahead of the values' take kernel, whatever their encoding.
180+
#[derive(Debug)]
181+
pub(crate) struct DictConstantCodesKernel;
182+
183+
impl ExecuteParentKernel<Constant> for DictConstantCodesKernel {
184+
type Parent = Dict;
185+
186+
fn execute_parent(
187+
&self,
188+
array: ArrayView<'_, Constant>,
189+
parent: <Self::Parent as Matcher>::Match<'_>,
190+
child_idx: usize,
191+
ctx: &mut ExecutionCtx,
192+
) -> VortexResult<Option<ArrayRef>> {
193+
if child_idx != 0 || parent.is_empty() {
194+
return Ok(None);
195+
}
196+
let value = match array.scalar().as_primitive().as_::<usize>() {
197+
Some(code) => parent
198+
.values()
199+
.execute_scalar(code, ctx)?
200+
.cast(parent.dtype())?,
201+
None => Scalar::null(parent.dtype().as_nullable()),
202+
};
203+
Ok(Some(ConstantArray::new(value, parent.len()).into_array()))
204+
}
205+
}

‎vortex-array/src/arrays/dict/tests.rs‎

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,24 @@
22
// SPDX-FileCopyrightText: Copyright the Vortex contributors
33

44
use vortex_buffer::buffer;
5+
use vortex_error::VortexResult;
56

67
use super::DictArray;
78
use crate::IntoArray;
89
use crate::VortexSessionExecute;
910
use crate::array_session;
1011
use crate::arrays::BoolArray;
12+
use crate::arrays::ConstantArray;
1113
use crate::arrays::FixedSizeListArray;
1214
use crate::arrays::ListArray;
1315
use crate::arrays::PrimitiveArray;
1416
use crate::arrays::VarBinArray;
1517
use crate::assert_arrays_eq;
1618
use crate::builders::builder_with_capacity_in;
19+
use crate::dtype::DType;
20+
use crate::dtype::Nullability;
21+
use crate::dtype::PType;
22+
use crate::scalar::Scalar;
1723
use crate::validity::Validity;
1824

1925
#[test]
@@ -30,7 +36,7 @@ fn test_scalar_at_null_code() {
3036
}
3137

3238
#[test]
33-
fn test_canonicalize_dict_with_all_null_array_validity_codes() -> vortex_error::VortexResult<()> {
39+
fn test_canonicalize_dict_with_all_null_array_validity_codes() -> VortexResult<()> {
3440
let mut ctx = array_session().create_execution_ctx();
3541
let codes = PrimitiveArray::new(
3642
buffer![0u32, 1],
@@ -133,3 +139,42 @@ fn test_dict_list_dict_display() {
133139
"[[\"Bonjour\", \"Bonjour\", null], [\"你好\", \"Hello\"], null, [\"Hello\", \"Hello\"], null, []]"
134140
)
135141
}
142+
143+
#[test]
144+
fn canonicalize_constant_nullable_code_over_non_nullable_values() -> VortexResult<()> {
145+
let mut ctx = array_session().create_execution_ctx();
146+
let code = Scalar::primitive(1u8, Nullability::Nullable);
147+
let dict = DictArray::try_new(
148+
ConstantArray::new(code, 3).into_array(),
149+
VarBinArray::from_iter(
150+
["a", "long enough to live out of line"].map(Some),
151+
DType::Utf8(Nullability::NonNullable),
152+
)
153+
.into_array(),
154+
)?;
155+
let expected = VarBinArray::from_iter(
156+
[Some("long enough to live out of line"); 3],
157+
DType::Utf8(Nullability::Nullable),
158+
);
159+
assert_arrays_eq!(dict, expected, &mut ctx);
160+
Ok(())
161+
}
162+
163+
#[test]
164+
fn canonicalize_constant_null_code() -> VortexResult<()> {
165+
let mut ctx = array_session().create_execution_ctx();
166+
let dict = DictArray::try_new(
167+
ConstantArray::new(
168+
Scalar::null(DType::Primitive(PType::U8, Nullability::Nullable)),
169+
3,
170+
)
171+
.into_array(),
172+
buffer![10i32, 20, 30].into_array(),
173+
)?;
174+
assert_arrays_eq!(
175+
dict,
176+
PrimitiveArray::from_option_iter([None::<i32>, None, None]),
177+
&mut ctx
178+
);
179+
Ok(())
180+
}

‎vortex-array/src/arrays/dict/vtable/kernel.rs‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@ use vortex_session::VortexSession;
55

66
use crate::ArrayVTable;
77
use crate::arrays::Chunked;
8+
use crate::arrays::Constant;
89
use crate::arrays::Dict;
910
use crate::arrays::dict::TakeExecuteAdaptor;
11+
use crate::arrays::dict::take::DictConstantCodesKernel;
1012
use crate::optimizer::kernels::ArrayKernelsExt;
1113
use crate::scalar_fn::ScalarFnVTable;
1214
use crate::scalar_fn::fns::binary::Binary;
@@ -18,6 +20,7 @@ pub(crate) fn initialize(session: &VortexSession) {
1820
let kernels = session.kernels();
1921
kernels.register_execute_parent_kernel(Binary.id(), Dict, CompareExecuteAdaptor(Dict));
2022
kernels.register_execute_parent_kernel(Dict.id(), Chunked, TakeExecuteAdaptor(Chunked));
23+
kernels.register_execute_parent_kernel(Dict.id(), Constant, DictConstantCodesKernel);
2124
kernels.register_execute_parent_kernel(Dict.id(), Dict, TakeExecuteAdaptor(Dict));
2225
kernels.register_execute_parent_kernel(FillNull.id(), Dict, FillNullExecuteAdaptor(Dict));
2326
}

0 commit comments

Comments
 (0)