Skip to content

Commit 254f91b

Browse files
perf[array]: add the SimplifyCache to optimize (#7948)
Cache optimisations to array in try_optimize on the way down --------- Signed-off-by: Joe Isaacs <joe.isaacs@live.co.uk>
1 parent 13c06b8 commit 254f91b

4 files changed

Lines changed: 118 additions & 16 deletions

File tree

vortex-array/Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,11 @@ name = "expr_case_when"
143143
path = "benches/expr/case_when_bench.rs"
144144
harness = false
145145

146+
[[bench]]
147+
name = "expr_optimize"
148+
path = "benches/expr/optimize_bench.rs"
149+
harness = false
150+
146151
[[bench]]
147152
name = "chunked_dict_builder"
148153
harness = false
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
// SPDX-FileCopyrightText: Copyright the Vortex contributors
3+
4+
#![expect(clippy::unwrap_used)]
5+
#![expect(clippy::cast_possible_truncation)]
6+
7+
use divan::Bencher;
8+
use vortex_array::dtype::DType;
9+
use vortex_array::dtype::Nullability;
10+
use vortex_array::dtype::PType;
11+
use vortex_array::dtype::StructFields;
12+
use vortex_array::expr::Expression;
13+
use vortex_array::expr::eq;
14+
use vortex_array::expr::get_item;
15+
use vortex_array::expr::lit;
16+
use vortex_array::expr::or;
17+
use vortex_array::expr::root;
18+
19+
fn main() {
20+
divan::main();
21+
}
22+
23+
fn struct_scope() -> DType {
24+
DType::Struct(
25+
StructFields::new(
26+
["x"].into(),
27+
vec![DType::Primitive(PType::I32, Nullability::NonNullable)],
28+
),
29+
Nullability::NonNullable,
30+
)
31+
}
32+
33+
fn build_or_chain(n: usize) -> Expression {
34+
let base = eq(get_item("x", root()), lit(0i32));
35+
(1..n).fold(base, |acc, i| {
36+
or(acc, eq(get_item("x", root()), lit(i as i32)))
37+
})
38+
}
39+
40+
#[divan::bench(args = [200])]
41+
fn optimize_or_chain(bencher: Bencher, n: usize) {
42+
let expr = build_or_chain(n);
43+
let scope = struct_scope();
44+
bencher.bench(|| expr.optimize_recursive(&scope).unwrap());
45+
}

vortex-array/public-api.lock

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12860,8 +12860,6 @@ pub fn vortex_array::expr::Expression::simplify(&self, &vortex_array::dtype::DTy
1286012860

1286112861
pub fn vortex_array::expr::Expression::simplify_untyped(&self) -> vortex_error::VortexResult<vortex_array::expr::Expression>
1286212862

12863-
pub fn vortex_array::expr::Expression::try_optimize(&self, &vortex_array::dtype::DType) -> vortex_error::VortexResult<core::option::Option<vortex_array::expr::Expression>>
12864-
1286512863
pub fn vortex_array::expr::Expression::try_optimize_recursive(&self, &vortex_array::dtype::DType) -> vortex_error::VortexResult<core::option::Option<vortex_array::expr::Expression>>
1286612864

1286712865
impl core::clone::Clone for vortex_array::expr::Expression

vortex-array/src/expr/optimize.rs

Lines changed: 68 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -29,18 +29,22 @@ impl Expression {
2929
/// 2. `simplify` - type-aware simplifications
3030
/// 3. `reduce` - abstract reduction rules via `ReduceNode`/`ReduceCtx`
3131
pub fn optimize(&self, scope: &DType) -> VortexResult<Expression> {
32+
let cache = SimplifyCache {
33+
scope,
34+
dtype_cache: RefCell::new(HashMap::new()),
35+
};
3236
Ok(self
3337
.clone()
34-
.try_optimize(scope)?
38+
.try_optimize(scope, &cache)?
3539
.unwrap_or_else(|| self.clone()))
3640
}
3741

3842
/// Try to optimize the root expression node only, returning None if no optimizations applied.
39-
pub fn try_optimize(&self, scope: &DType) -> VortexResult<Option<Expression>> {
40-
let cache = SimplifyCache {
41-
scope,
42-
dtype_cache: RefCell::new(HashMap::new()),
43-
};
43+
fn try_optimize(
44+
&self,
45+
scope: &DType,
46+
cache: &SimplifyCache<'_>,
47+
) -> VortexResult<Option<Expression>> {
4448
let reduce_ctx = ExpressionReduceCtx {
4549
scope: scope.clone(),
4650
};
@@ -67,7 +71,7 @@ impl Expression {
6771
}
6872

6973
// Try simplify (typed)
70-
if let Some(simplified) = current.scalar_fn().simplify(&current, &cache)? {
74+
if let Some(simplified) = current.scalar_fn().simplify(&current, cache)? {
7175
current = simplified;
7276
changed = true;
7377
any_optimizations = true;
@@ -114,11 +118,28 @@ impl Expression {
114118

115119
/// Try to optimize the entire expression tree recursively.
116120
pub fn try_optimize_recursive(&self, scope: &DType) -> VortexResult<Option<Expression>> {
121+
let cache = SimplifyCache {
122+
scope,
123+
dtype_cache: RefCell::new(HashMap::new()),
124+
};
125+
let result = self.try_optimize_recursive_inner(scope, &cache)?;
126+
127+
// Apply the between optimization once at the top level only.
128+
// TODO(ngates): remove the "between" optimization, or rewrite it to not always convert
129+
// to CNF?
130+
Ok(Some(find_between(result.unwrap_or_else(|| self.clone()))))
131+
}
132+
133+
fn try_optimize_recursive_inner(
134+
&self,
135+
scope: &DType,
136+
cache: &SimplifyCache<'_>,
137+
) -> VortexResult<Option<Expression>> {
117138
let mut current = self.clone();
118139
let mut any_optimizations = false;
119140

120141
// First optimize the root
121-
if let Some(optimized) = current.clone().try_optimize(scope)? {
142+
if let Some(optimized) = current.clone().try_optimize(scope, cache)? {
122143
current = optimized;
123144
any_optimizations = true;
124145
}
@@ -127,7 +148,7 @@ impl Expression {
127148
let mut new_children = Vec::with_capacity(current.children().len());
128149
let mut any_child_optimized = false;
129150
for child in current.children().iter() {
130-
if let Some(optimized) = child.try_optimize_recursive(scope)? {
151+
if let Some(optimized) = child.try_optimize_recursive_inner(scope, cache)? {
131152
new_children.push(optimized);
132153
any_child_optimized = true;
133154
} else {
@@ -140,15 +161,11 @@ impl Expression {
140161
any_optimizations = true;
141162

142163
// After updating children, try to optimize root again
143-
if let Some(optimized) = current.clone().try_optimize(scope)? {
164+
if let Some(optimized) = current.clone().try_optimize(scope, cache)? {
144165
current = optimized;
145166
}
146167
}
147168

148-
// TODO(ngates): remove the "between" optimization, or rewrite it to not always convert
149-
// to CNF?
150-
let current = find_between(current);
151-
152169
if any_optimizations {
153170
Ok(Some(current))
154171
} else {
@@ -294,3 +311,40 @@ impl ReduceCtx for ExpressionReduceCtx {
294311
}))
295312
}
296313
}
314+
315+
#[cfg(test)]
316+
mod tests {
317+
use vortex_error::VortexResult;
318+
319+
use crate::dtype::DType;
320+
use crate::dtype::Nullability;
321+
use crate::dtype::PType;
322+
use crate::dtype::StructFields;
323+
use crate::expr::eq;
324+
use crate::expr::get_item;
325+
use crate::expr::lit;
326+
use crate::expr::or;
327+
use crate::expr::root;
328+
329+
#[test]
330+
fn optimize_or_chain_correctness() -> VortexResult<()> {
331+
let expr = or(
332+
eq(get_item("x", root()), lit(1i32)),
333+
eq(get_item("x", root()), lit(2i32)),
334+
);
335+
let scope = DType::Struct(
336+
StructFields::new(
337+
["x"].into(),
338+
vec![DType::Primitive(PType::I32, Nullability::NonNullable)],
339+
),
340+
Nullability::NonNullable,
341+
);
342+
let optimized = expr.optimize_recursive(&scope)?;
343+
344+
let s = optimized.to_string();
345+
assert!(s.contains("$.x"), "expected $.x in {s}");
346+
assert!(s.contains("1i32") || s.contains('1'), "expected 1 in {s}");
347+
assert!(s.contains("2i32") || s.contains('2'), "expected 2 in {s}");
348+
Ok(())
349+
}
350+
}

0 commit comments

Comments
 (0)