Repository navigation
ScalarFnVtable's validity is parametrized by ReduceNode - #9932
Conversation
Merging this PR will degrade performance by 3.06%
|
93069bc to
4ca7b44
Compare
4ca7b44 to
4f5168a
Compare
4f5168a to
b64d2be
Compare
b64d2be to
4f5168a
Compare
fb7d5ce to
179eb09
Compare
4f5168a to
3922c36
Compare
3b9ed1a to
1247547
Compare
3922c36 to
4b33f7f
Compare
4b33f7f to
dfa256f
Compare
75e547c to
a5b5904
Compare
dfa256f to
c9e21c3
Compare
|
In addition to the discussion: there are three places where you can do the
single execute step once all symbolic reduction rules are done:
1. Executor pipeline. We can't do this right now since ScalarFn::execute returns
"done", and teaching it to do ExecuteSlot's is a separate big change.
3. IsNotNull::validity() (chosen). ScalarFn::validity() stays lazy, but
IsNotNull::validity() may do execution, and you need scalar special handing in
order to avoid infinite recursion.
2. ScalarFn::validity(). No scalar special handling, but validity() isn't fully
lazy, as it may do execution
|
|
the flaw of current API is that repeated probe cache is same for both masks
and values, so you can't just cache the validity.
We've agreed I'd fix this separately. There are two separate changes to remove
both checks for .is::<ScalarFn>().
1. RepeatedProbe should be able to ask the array implementing OperationsVTable
for validity, and the array should be responsible for providing the validity,
canonicalized or lazy.
2. probe_scalar requires the caller has checked the validity. We can remove this
precondition and instead make probe_scalar check the validity itself when it
wants.
|
|
Regarding IsNotNull special handling: there is an issue indeed if you add a new
object, say, WindowFunction, and this WindowFunction has special handling for
is_not_null, there will be an issue with cycles again, and we'd need to handle
this in IsNotNull execution, which is bad.
So, as a temporary measure, we'll merge a solution where Array.validity() may do
an execute step, and then apply an optimization on top of it: a special internal
array which is constructed when ScalarFn.validity() is Irreducible. On execution
of this special array it replaces itself with
`is_not_null(x.execute::<ArrayRef>())`.
|
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)) |
There was a problem hiding this comment.
could avoid the unwrap and alloc
| expr: &BoundExpression, | ||
| _session: &VortexSession, | ||
| ) -> VortexResult<Option<BoundExpression>> { | ||
| Ok(literal_truth(expr).map(|value| lit(!value))) |
There was a problem hiding this comment.
hopefully we avoid an alloc for a lit bool
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>
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 unifiedrules 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>returnsthese 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 toevaluate x's offsets.
[2]
zipis a special case because it satisfies the return type condition butisn't strict.