Skip to content

Optimize BinaryExpr Evaluation with Short-Circuiting for AND/OR Operators #15648

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 33 commits into from

Conversation

kosiew
Copy link
Contributor

@kosiew kosiew commented Apr 9, 2025

Which issue does this PR close?

Rationale for this change

This PR improves the short-circuit evaluation logic for logical binary expressions (AND, OR) in DataFusion's physical expression layer mentioned in #15636.

What changes are included in this PR?

  • Introduced an explicit ShortCircuitStrategy enum to differentiate between short-circuit return cases (ReturnLeft, ReturnRight, None)
  • Rewrote the check_short_circuit function to:
    • Consider scalar vs. array inputs
    • Handle both AND and OR semantics correctly
    • Account for nulls and return None strategy when data is ambiguous
  • Updated BinaryExpr::evaluate to leverage the new short-circuit strategy cleanly
  • Added new benchmark case (all_false OR scenario) to test and observe non-short-circuit behavior
  • Expanded and refactored test coverage for:
    • Scalar true/false/null cases
    • All true/false array scenarios
    • Nullable arrays and null-only arrays

Are these changes tested?

✅ Yes, comprehensive unit tests were added for the new short-circuit logic, including scalar and array edge cases, null handling, and all valid operator configurations.

Are there any user-facing changes?

No direct user-facing API changes, but performance and correctness will improve for queries involving logical expressions. This is an internal enhancement to the expression evaluation engine.

kosiew added 13 commits April 9, 2025 10:06
- Delay evaluation of the right-hand side (RHS) unless necessary.
- Optimize short-circuiting for `Operator::And` and `Operator::Or` by checking LHS alone first.
- Introduce `get_short_circuit_result` function to determine short-circuit conditions based on LHS and RHS.
- Update tests to cover various short-circuit scenarios for both `AND` and `OR` operations.
…esult and update assertions

- Renamed the test function for clarity.
- Updated assertions to use get_short_circuit_result instead of check_short_circuit.
- Added additional test cases for AND and OR operations with expected results.
…lt function for null

- Updated AND and OR short-circuit conditions to only trigger when all values are either false or true, respectively, and there are no nulls in the array.
- Adjusted test case to reflect the change in expected output.
…d enhance documentation for get_short_circuit_result
@github-actions github-actions bot added the physical-expr Changes to the physical-expr crates label Apr 9, 2025
@alamb
Copy link
Contributor

alamb commented Apr 9, 2025

Thanks @kosiew -- have you run the benchmarks and do you have any results to share?

kosiew added 15 commits April 9, 2025 18:44
…Expr to optimize logical operations"

This reverts commit a62df47.
…luation in BinaryExpr

- Replaced the lazy evaluation of the right-hand side (RHS) with immediate evaluation based on short-circuiting logic.
- Introduced a new function `check_short_circuit` to determine if short-circuiting can be applied for logical operators.
- Updated the logic to return early for `Operator::And` and `Operator::Or` based on the evaluation of the left-hand side (LHS) and the conditions of the RHS.
- Improved clarity and efficiency of the short-circuit evaluation process by eliminating unnecessary evaluations.
…nction

- Simplified logic for AND/OR operations by prioritizing false/true counts to enhance performance.
- Updated documentation to reflect changes in array handling techniques.
…_short_circuit logic

- Introduced a new helper function `count_boolean_values` to count true and false values in a BooleanArray, improving readability and performance.
- Updated `check_short_circuit` to utilize the new helper function for counting, reducing redundant operations and enhancing clarity in the evaluation logic for AND/OR operations.
- Adjusted comments for better understanding of the short-circuiting conditions based on the new counting mechanism.
…ze check_short_circuit logic"

This reverts commit e2b9f77.
…tors

- Renamed `arg` to `lhs` for clarity in the `get_short_circuit_result` function.
- Updated handling of Boolean data types to return `None` for null values.
- Simplified short-circuit checks for AND/OR operations by consolidating logic.
- Enhanced readability and maintainability of the code by restructuring match statements.
@kosiew
Copy link
Contributor Author

kosiew commented Apr 10, 2025

hi @alamb

Here are the benchmark results after incorporating @acking-you 's enum suggestion

image

@alamb
Copy link
Contributor

alamb commented Apr 10, 2025

Is there anyway to get performance back for the bencmarks where it slowed down?

@kosiew
Copy link
Contributor Author

kosiew commented Apr 11, 2025

Findings after trying many optimisation attempts:

  1. benchmark results are not stable. At the µs scale, many factors(besides the code itself) can swing the results.
  2. only some changes are statistically significant (p < 0.05) and I modified my script to retrieve only significant results

Below are some of the significant (unstable) changes, comparing this branch against main:

image image image

As an illustration of the unstable benchmark results, I ran cargo bench on main to compare against a saved main baseline and obtained these significant changes:

image image

The above seem to indicate that main branch without any changes has also regressed.

@kosiew
Copy link
Contributor Author

kosiew commented Apr 11, 2025

The mean regressions affect idealized (all true, all false) benchmarks, not the complex mixed-null or partial-match cases real queries often encounter.

The gains happen in more practical scenarios, less extreme cases.

The code is now semantically correct with the ShortCircuitStrategy enum and more maintainable.

The unit tests cover edge cases better than before, including nulls and scalars.

Should we merge this?

Comment on lines +414 to +424
// If the left-hand side is an array and the right-hand side is a non-null scalar, try the optimized kernel.
if let (ColumnarValue::Array(array), ColumnarValue::Scalar(ref scalar)) =
(&lhs, &rhs)
{
if !scalar.is_null() {
if let Some(result_array) =
self.evaluate_array_scalar(array, scalar.clone())?
{
let final_array = result_array
.and_then(|a| to_result_type_array(&self.op, a, &result_type));
return final_array.map(ColumnarValue::Array);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

a rewrite while trying to optimize this function.

@berkaysynnada
Copy link
Contributor

Thank you @kosiew. I'm a bit busy but I'll review this as soon as I find some time

@alamb
Copy link
Contributor

alamb commented Apr 11, 2025

@kosiew -- I wonder if you saw this post from @Dandandan : #15631 (comment)

It seems a simpler way to improve performance 🤔 (though I think this PR woudl still apply)

@kosiew
Copy link
Contributor Author

kosiew commented Apr 14, 2025

Closing this.
@acking-you improves this significantly in #15694

@kosiew kosiew closed this Apr 14, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
physical-expr Changes to the physical-expr crates
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add more short-circuit optimization scenarios for OR and AND
3 participants