Skip to content

ScalarFnVtable's validity is parametrized by ReduceNode - #9932

Merged
myrrc merged 22 commits into
developfrom
myrrc/lazy-scalarfn-validity
Sep 30, 2026
Merged

myrrc merged 22 commits into
developfrom
myrrc/lazy-scalarfn-validity

Conversation

@myrrc

@myrrc myrrc commented Sep 17, 2026 •

Copy link
Copy Markdown
Contributor

ScalarFn validity() recursively canonicalizes whole subgraph starting from
itself. This is very inefficient. For example, if you have a single probe of
element at index i, the whole array will be canonicalized. What's worse, probing
and scalar_at check element's validity before getting the element, and for some
functions like list_contains this evaluates the function[1], so for every probe you
end up evaluating the function twice.

We already have ScalarFn reduce<ReduceNode> which allows you to write unified
rules over both expressions and arrays. This change makes validity also
parametrised by a ReduceNode parameter and also adds a validity() method on the
ReduceNode itself.

There are two ways how you can reduce a node. First is symbolic reduction which
happens without executing anything - think replacing IsNotNull(x) with
x.validity. If you can't reduce the node anymore, you do one step of evaluation -
think calling execute() - and then repeat. This is how our executor works in
general, and same logic may be applied to ReduceNodes. validity<T> returns
these two cases: a node can either be symbolically reduced to other node, or is
Irreducible in which case you need to execute.

While probing we then avoid second function evaluation - if expression/array's
validity() is Irreducible, evaluating function once is sufficient.

We also want the execution of validity() to be lazy - it should never fully
canonicalize. Unfortunately, ScalarFn::execute() ultimately returns "done" so
you can't express "give me my child and execute it one step". Thus this single
step execution happens inside an internal Array which is exactly "validity of an
Irreducible ScalarFn array".

Expression's validity() for a scalar function returns either a reduced
expression or IsNotNull(self). In the latter case we want to avoid infinite
recursion when replacing IsNotNull(x) -> x.validity() so the reduction rule
does nothing if the expression returned Irreducible.

To avoid manually rewriting all existing validity rules, we can use the
predicate "if a function is strict[2] and all nullable argument's return types
are non-nullable, then its validity is and() over child validities". This
removes a significant number of overloads.

This change also adds a is_null/is_not_null reduce rule which is
#10025 but reimplemented
on top of generic reduce<> which requires validity<>.

Given X = bool?, Y = bool, it symbolically reduces

is_null(and(X, Y)) -> and(is_null(X), Y)
is_not_null(and(X, Y)) -> or(X.validitY, not(Y))
is_null(or(X, Y)) -> and(is_null(X), not(Y))
is_not_null(or(X, Y)) -> or(X.validitY, Y)

during array.optimize(). These optimizations lower the complexity of subtree
evaluation. As an example, is_null(and(x, y)) is 5 computations: x, y, and(x,
y), and(x, y).validity(), not(and(x, y).validity()). We replace it with 4:
x.validity, y, not(x.validity), and(not(x.validity()), y). Given x.validity is
either as expensive or less expensive than x, this is a net benefit.

This change also doesn't execute the short-circuited array in fill_null. As
scalar fn validity is now lazy, we can safely return the lazy array with added
cast.

This change also adds the literal stats falsifier. As scalar function evaluation
may now fold into a constant, we need to falsify the partition based on this
literal.

[1] list_contains([], null) = false, #10028, but to know this, you need to
evaluate x's offsets.
[2] zip is a special case because it satisfies the return type condition but
isn't strict.

@myrrc myrrc added the changelog/feature A new feature label Sep 17, 2026
@codspeed

codspeed Bot commented Sep 17, 2026 •

Copy link
Copy Markdown

Merging this PR will degrade performance by 3.06%

⚠️ Unknown Walltime execution environment detected

Using the Walltime instrument on standard Hosted Runners will lead to inconsistent data.

For the most accurate results, we recommend using CodSpeed Macro Runners: bare-metal machines fine-tuned for performance measurement consistency.

⚠️ Different runtime environments detected

Some benchmarks with significant performance changes were compared across different runtime environments,
which may affect the accuracy of the results.

Open the report in CodSpeed to investigate

⚡ 3 improved benchmarks
❌ 5 regressed benchmarks
✅ 2054 untouched benchmarks
🆕 8 new benchmarks
⏩ 503 skipped benchmarks1

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Mode Benchmark BASE HEAD Efficiency
❌ WallTime mul_u64_nonnull_neon 29.6 µs 39.4 µs -24.99%
❌ Simulation execute_scalar_struct_wide 403.5 µs 470.2 µs -14.19%
❌ WallTime multiply_shapes_neon[(32768, PerRowPerRow)] 33.3 µs 38.5 µs -13.74%
❌ WallTime mul_i64_nonnull_neon 33.5 µs 38.5 µs -13.03%
❌ Simulation execute_scalar_struct_simple 93.4 µs 106.5 µs -12.31%
⚡ Simulation bench_dict_mask[(0.9, 0.9)] 771.7 µs 516.1 µs +49.53%
⚡ WallTime dict_canonicalize_gt_u8_neon[1000000] 547 µs 488.8 µs +11.91%
⚡ WallTime scalar_subtract_neon 13.9 µs 12.6 µs +10.03%
🆕 Simulation probe_scalar_fn_once[vortex.binary(bool?, len=100000)] N/A 303.3 µs N/A
🆕 Simulation probe_scalar_fn_once[vortex.binary(i64?, len=100000)] N/A 720.1 µs N/A
🆕 Simulation probe_scalar_fn_repeated[vortex.binary(bool?, len=100000)] N/A 315.8 µs N/A
🆕 Simulation probe_scalar_fn_repeated[vortex.binary(i64?, len=100000)] N/A 728.5 µs N/A
🆕 Simulation probe_scalar_fn_valid_once[vortex.binary(bool?, len=100000)] N/A 372.6 µs N/A
🆕 Simulation probe_scalar_fn_valid_once[vortex.binary(i64?, len=100000)] N/A 411.8 µs N/A
🆕 Simulation probe_scalar_fn_valid_repeated[vortex.binary(bool?, len=100000)] N/A 216.6 µs N/A
🆕 Simulation probe_scalar_fn_valid_repeated[vortex.binary(i64?, len=100000)] N/A 67 µs N/A

Tip

Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.


Comparing myrrc/lazy-scalarfn-validity (2b9f8c6) with develop (a96e0a8)

Open in CodSpeed

Footnotes

  1. 503 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports. ↩

@myrrc
myrrc force-pushed the myrrc/lazy-scalarfn-validity branch from 93069bc to 4ca7b44 Compare September 21, 2026 11:06
@myrrc
myrrc added this pull request to stack #9982 September 22, 2026 16:54
@myrrc
myrrc removed this pull request from stack #9982 September 22, 2026 16:55
@myrrc
myrrc force-pushed the myrrc/lazy-scalarfn-validity branch from 4ca7b44 to 4f5168a Compare September 22, 2026 16:55
@myrrc
myrrc changed the base branch from develop to myrrc/isnull-reduce September 22, 2026 16:55
@myrrc
myrrc added this pull request to stack #9983 September 22, 2026 16:55
Base automatically changed from myrrc/isnull-reduce to develop September 23, 2026 10:24
@myrrc
myrrc force-pushed the myrrc/lazy-scalarfn-validity branch from 4f5168a to b64d2be Compare September 23, 2026 10:24
@myrrc
myrrc removed this pull request from stack #9983 September 23, 2026 10:24
@myrrc
myrrc force-pushed the myrrc/lazy-scalarfn-validity branch from b64d2be to 4f5168a Compare September 23, 2026 10:25
@myrrc
myrrc changed the base branch from develop to myrrc/binary-array-reduce September 23, 2026 10:25
@myrrc
myrrc added this pull request to stack #9998 September 23, 2026 10:26
@myrrc
myrrc force-pushed the myrrc/binary-array-reduce branch from fb7d5ce to 179eb09 Compare September 23, 2026 13:53
@myrrc
myrrc force-pushed the myrrc/lazy-scalarfn-validity branch from 4f5168a to 3922c36 Compare September 23, 2026 13:53
@myrrc
myrrc force-pushed the myrrc/binary-array-reduce branch 2 times, most recently from 3b9ed1a to 1247547 Compare September 23, 2026 14:21
@myrrc
myrrc force-pushed the myrrc/lazy-scalarfn-validity branch from 3922c36 to 4b33f7f Compare September 24, 2026 09:57
@myrrc
myrrc removed this pull request from stack #9998 September 24, 2026 09:57
@myrrc
myrrc changed the base branch from myrrc/binary-array-reduce to myrrc/isnull-and-reduce September 24, 2026 10:00
@myrrc
myrrc added this pull request to stack #10026 September 24, 2026 10:00
@myrrc
myrrc force-pushed the myrrc/lazy-scalarfn-validity branch from 4b33f7f to dfa256f Compare September 24, 2026 10:54
@myrrc
myrrc force-pushed the myrrc/isnull-and-reduce branch from 75e547c to a5b5904 Compare September 24, 2026 10:54
@myrrc myrrc added changelog/break A breaking API change and removed changelog/feature A new feature labels Sep 24, 2026
@myrrc myrrc changed the title Lazy ScalarFn::validity ParentExecuteKernel for validity Sep 24, 2026
@myrrc myrrc changed the title ParentExecuteKernel for validity ExecuteParentKernel for validity Sep 24, 2026
@myrrc
myrrc force-pushed the myrrc/lazy-scalarfn-validity branch from dfa256f to c9e21c3 Compare September 25, 2026 12:13
Comment thread vortex-array/src/scalar_fn/fns/binary/mod.rs
Comment thread vortex-array/src/scalar_fn/fns/between/mod.rs
Comment thread vortex-array/benches/scalar_fn_probe.rs
Comment thread vortex-array/src/array/probe/array.rs Outdated
Comment thread vortex-array/src/array/probe/repeated.rs
Comment thread vortex-array/src/scalar_fn/vtable.rs Outdated
Comment thread vortex-array/src/scalar_fn/vtable.rs
Comment thread vortex-array/src/scalar_fn/vtable.rs
Comment thread vortex-array/src/scalar_fn/vtable.rs
Comment thread vortex-array/src/scalar_fn/vtable.rs
@myrrc

myrrc commented Sep 28, 2026 via email

Copy link
Copy Markdown
Contributor Author

Signed-off-by: Mikhail Kot <mikhail@spiraldb.com>
@myrrc
myrrc requested a review from joseph-isaacs September 28, 2026 16:42
@myrrc
myrrc enabled auto-merge (squash) September 28, 2026 16:42
@myrrc

myrrc commented Sep 29, 2026 via email

Copy link
Copy Markdown
Contributor Author

Comment thread vortex-array/src/array/probe/repeated.rs
Comment thread vortex-array/src/array/probe/repeated.rs Outdated
Comment thread vortex-array/src/array/probe/array.rs
@myrrc

myrrc commented Sep 29, 2026 via email

Copy link
Copy Markdown
Contributor Author

Signed-off-by: Mikhail Kot <mikhail@spiraldb.com>
@myrrc
myrrc requested a review from joseph-isaacs September 29, 2026 17:09
Signed-off-by: Mikhail Kot <mikhail@spiraldb.com>
Comment thread vortex-array/src/array/probe/repeated.rs
Comment thread vortex-array/src/array/probe/repeated.rs
Comment thread vortex-array/src/expr/bound_expression.rs
Comment thread vortex-array/src/stats/rewrite/builtins.rs
Signed-off-by: Mikhail Kot <mikhail@spiraldb.com>
@myrrc
myrrc requested a review from joseph-isaacs September 30, 2026 10:43
Signed-off-by: Mikhail Kot <mikhail@spiraldb.com>
Signed-off-by: Mikhail Kot <mikhail@spiraldb.com>
expr: &BoundExpression,
_session: &VortexSession,
) -> VortexResult<Option<BoundExpression>> {
Ok(literal_truth(expr).map(lit))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could avoid the unwrap and alloc

expr: &BoundExpression,
_session: &VortexSession,
) -> VortexResult<Option<BoundExpression>> {
Ok(literal_truth(expr).map(|value| lit(!value)))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hopefully we avoid an alloc for a lit bool

@myrrc
myrrc disabled auto-merge September 30, 2026 11:02
@myrrc
myrrc enabled auto-merge (squash) September 30, 2026 11:02
@myrrc
myrrc merged commit 91285b8 into develop Sep 30, 2026
101 of 102 checks passed
@myrrc
myrrc deleted the myrrc/lazy-scalarfn-validity branch September 30, 2026 11:12
myrrc added a commit that referenced this pull request Oct 7, 2026
Depends on #9932.

We have lazy ScalarFn::validity() now, so fill_null is also lazy. Thus
there's
no need in special null_as_false. Deprecate it and replace all usages
with
fill_null(false) except for zone map pruning. Pruning work is very fast,
so
just constructing FillNull node and then canonicalizing is slower than
canonicalizing and intersecting the bitbuffer.
So pruning keeps the current implementation which is now private.

---------

Signed-off-by: Mikhail Kot <mikhail@spiraldb.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

changelog/break A breaking API change

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants