Uh oh!
There was an error while loading. Please reload this page.
Add short circuit evaluation for AND and OR - #15462
Conversation
acking-you
commented
Mar 27, 2025
Some tests failed, so let me take a look at what exactly is going on. |
ctsk
commented
Mar 28, 2025
I think one issue is that the short-circuit logic is not handling cases where the the |
acking-you
commented
Mar 28, 2025
Thank you very much for your hint, it will be very helpful for me to fix these tests! |
acking-you
commented
Mar 28, 2025
After taking a closer look, in fact, the situation you mentioned does not actually lead to the short-circuit optimization logic. |
ctsk
commented
Mar 28, 2025
You're absolutely right, I got my logic wrong there. Embarrasing! |
acking-you
commented
Mar 28, 2025
It's okay. You've also taught me a lot. When I first started writing this, I really didn't consider the case of null |
acking-you
commented
Mar 28, 2025
Hello @alamb, the optimization SQL and documentation related to this PR have been completed, and all tests have passed. We may need to formally verify the performance, but I'm not quite sure how to do that (I can only run it locally). |
alamb
left a comment
There was a problem hiding this comment.
Thanks @acking-you -- this looks really nice.
I think this needs some tests but otherwise it is looking quite nice.
Also, could you please add the new Q6 benchmark in a separate PR so I can more easily run my benchmark scripts before/after your code change?
| ### Q6: How many social shares meet complex multi-stage filtering criteria? | ||
| **Question**: What is the count of sharing actions from iPhone mobile users on specific social networks, within common timezones, participating in seasonal campaigns, with high screen resolutions and closely matched UTM parameters? | ||
| **Important Query Properties**: Simple filter with high-selectivity, Costly string matching, A large number of filters with high overhead are positioned relatively later in the process |
| fn evaluate(&self, batch: &RecordBatch) -> Result<ColumnarValue> { | ||
| use arrow::compute::kernels::numeric::*; | ||
| fn check_short_circuit(arg: &ColumnarValue, op: &Operator) -> bool { |
There was a problem hiding this comment.
Thanks @acking-you -- this looks great
Is there any reason to have this function defined in the evaluate method? I think we could just make it a normal function and reduce the nesting level
There was a problem hiding this comment.
If we find that this slows down some other performance we could also add some sort of heuristic check to calling false_count / true_count -- like for example if the rhs arg is "complex" (not a Column for example)
There was a problem hiding this comment.
Is there any reason to have this function defined in the evaluate method?
There was no particular reason. Maybe I couldn't find a suitable place to write it at the time, haha. Where do you think this function should be placed?
If we find that this slows down some other performance we could also add some sort of heuristic check to calling false_count / true_count -- like for example if the rhs arg is "complex" (not a Column for example)
I also agree that
There was a problem hiding this comment.
I've moved the function outside and added some comments.
Uh oh!
There was an error while loading. Please reload this page.
acking-you
commented
Mar 29, 2025
Okey,I got it.Do you mean that Q6 and its related description in the current branch need to be completed with a separate PR? But this way, it seems that you would still need to cherry-pick Q6 to the corresponding branch when testing. |
027a772 to
5de0f36Compare…ultiple benchmarks (apache#14642) * Add support --mem-pool-type and --memory-limit options for all benchmarks * Add --sort-spill-reservation-bytes option
* Add unit tests to FFI_ExecutionPlan * Add unit tests for FFI table source * Add round trip tests for volatility * Add unit tests for FFI insert op * Simplify string generation in unit test Co-authored-by: Andrew Lamb <andrew@nerdnetworks.org> * Fix drop of borrowed value --------- Co-authored-by: Andrew Lamb <andrew@nerdnetworks.org>
…ultiple benchmarks (apache#14642) * Add support --mem-pool-type and --memory-limit options for all benchmarks * Add --sort-spill-reservation-bytes option
* Add unit tests to FFI_ExecutionPlan * Add unit tests for FFI table source * Add round trip tests for volatility * Add unit tests for FFI insert op * Simplify string generation in unit test Co-authored-by: Andrew Lamb <andrew@nerdnetworks.org> * Fix drop of borrowed value --------- Co-authored-by: Andrew Lamb <andrew@nerdnetworks.org>
5de0f36 to
575c3f3Compareacking-you
commented
Mar 31, 2025
| match arg { | ||
| ColumnarValue::Array(array) => { | ||
| if let Ok(array) = as_boolean_array(&array) { | ||
| return array.false_count() == array.len(); |
There was a problem hiding this comment.
I remember this had some overhead (for calculating the counts) from a previous try.
I wonder if it helps to short optimize this expression (e.g. match until we get a chunk of the bitmap != 0)
There was a problem hiding this comment.
I wonder if it helps to short optimize this expression (e.g. match until we get a chunk of the bitmap != 0)
I think the overhead added here should be very small (the compiler optimization should work well), and the test results we discussed before were sometimes fast and sometimes slow (maybe noise).
Your suggestion of making an early judgment and returning false seems like a good idea, but I'm not sure if it will be effective.
The concern I have with this approach is that it requires adding an if condition inside the for loop, which will most likely disable the compiler's SIMD instruction optimization (I've encountered a similar situation before, and I had to manually unroll the SIMD...).
There was a problem hiding this comment.
Either way, we can use bool_and (https://docs.rs/arrow/latest/arrow/compute/fn.bool_and.html) and bool_or which operates on u64 values to test performance changes.
There was a problem hiding this comment.
Either way, we can use
bool_and(https://docs.rs/arrow/latest/arrow/compute/fn.bool_and.html) andbool_orwhich operates onu64values to test performance changes.
Thank you for your suggestion. I will try it later.
There was a problem hiding this comment.
Might be overkill, but one could try a sampling approach: Run the loop with the early exit for the first few chunks, and then switch over to the unconditional loop.
Almost seems like something the compiler could automagically do...
There was a problem hiding this comment.
Might be overkill, but one could try a sampling approach: Run the loop with the early exit for the first few chunks, and then switch over to the unconditional loop.
Thank you for your suggestion, but if we're only applying conditional checks to the first few blocks, then I feel this optimization might not be meaningful. If nearly all blocks can be filtered out by the preceding filter, the optimization will no longer be effective.
If we find that this slows down some other performance we could also add some sort of heuristic check to calling false_count / true_count -- like for example if the rhs arg is "complex" (not a Column for example)
I tend to agree with @alamb's point that if the overhead of verification is somewhat unacceptable, adopting some heuristic approaches would be better.
There was a problem hiding this comment.
I looked more carefully at bool_or and I do think it would be faster than this implementation on the case where there are some true values (as it stops as soon as it finds a single non zero): https://docs.rs/arrow/latest/arrow/compute/fn.bool_or.html
acking-you
commented
Apr 7, 2025
I sincerely apologize for the delay in updating this PR. I have now designed a detailed comparative test for the
It can be seen that when However, in other cases, using
# test true_count/false_count
TEST_BOOL_COUNT=1 cargo bench --bench boolean_op
# test bool_or/bool_and
cargo bench --bench boolean_opdetail benchmark code: https://github.com/apache/datafusion/pull/15462/files#diff-8710f6b44dd74240d19e6fcdfbf971c034f59cd48c022e51b07ed876a8cc7c5e |
alamb
commented
Apr 7, 2025
Thank you for your diligence @acking-you I think we should merge this PR in (with the count bits and the benchmarks) and file a follow on ticket to potentially improve the performance in some other way. I don't fully understand your performance results given that the two functions seem very similar -- maybe it has to do with option hanlding messing auto vectorization or something https://docs.rs/arrow-array/54.3.1/src/arrow_array/array/boolean_array.rs.html#160 |
Security audit failure is not related to this PR |
acking-you
commented
Apr 7, 2025
Yes, I was also quite surprised. The only difference in the implementation of these two functions might be whether or not |
alamb
left a comment
There was a problem hiding this comment.
Thank you so much @acking-you
I think the code in this PR is ready to go -- can you please adjust the benchmark code and then I'll merge this one in and file a follow on PR to explore other potential improvements?
Thanks again!
| cases | ||
| } | ||
| fn benchmark_boolean_ops(c: &mut Criterion) { |
There was a problem hiding this comment.
Can you please update these benchmarks for the performance of boolean expression evaluation? As of now, these are benchmarks for bool_and and bool_or?
In other words, make micro benchmarks for expressions like A AND B AND C AND D similar to what you added to the clickbench extended suite that shows the performance of expressions where the short circuit code improves performance.
We can then use those benchmarks to try out different ways to implementing the short circuting logic
| cases | ||
| } | ||
| fn benchmark_boolean_ops(c: &mut Criterion) { |
There was a problem hiding this comment.
Can you please update these benchmarks for the performance of boolean expression evaluation? As of now, these are benchmarks for bool_and and bool_or?
In other words, make micro benchmarks for expressions like A AND B AND C AND D similar to what you added to the clickbench extended suite that shows the performance of expressions where the short circuit code improves performance.
We can then use those benchmarks to try out different ways to implementing the short circuting logic
There was a problem hiding this comment.
in other words, make micro benchmarks for expressions like A AND B AND C AND D similar to what you added to the clickbench extended suite that shows the performance of expressions where the short circuit code improves performance.
This is a good idea
acking-you
commented
Apr 7, 2025
How can this be resolved? |
I think the likely part of it getting slower is short-circuiting whenever a For this case it might be interesting to compare it with For |
alamb
commented
Apr 7, 2025
There are ideas on there of how to resolve the isseu To be clear I don't think this is blocking this PR from merging The only thing I think is needed for this PR is to either
|
acking-you
commented
Apr 7, 2025
done |
acking-you
commented
Apr 7, 2025
I’m certain that the semantics of the RecordsRust code as follows: /// Scene 1: Counting all set bits (the number of 1s)#[no_mangle]#[inline(never)]pubfncount_ones(ba:&[u8]) -> usize{
ba.iter().map(|x| x.count_ones()asusize).sum()}/// Scene 2: Check if a bit is set#[no_mangle]#[inline(never)]pubfnfind_any(ba:&[u8]) -> bool{
ba.iter().any(|&x| x != 0)}Assembly code as follows: count_ones:testrsi,rsije .LBB0_1cmprsi,4jae .LBB0_5xorecx,ecxxoreax,eaxjmp .LBB0_8.LBB0_1:xoreax,eaxret.LBB0_5:movrcx,rsiandrcx,-4pxorxmm0,xmm0xoreax,eaxmovdqaxmm2, xmmword ptr [rip+ .LCPI0_0]movdqaxmm3, xmmword ptr [rip+ .LCPI0_1]movdqaxmm5, xmmword ptr [rip+ .LCPI0_2]pxorxmm4,xmm4pxorxmm1,xmm1.LBB0_6:movzxedx, word ptr [rdi+rax]movdxmm7,edxmovzxedx, word ptr [rdi+rax+2]movdxmm6,edxpunpcklbwxmm7,xmm0punpcklwdxmm7,xmm0punpckldqxmm7,xmm0movdqaxmm8,xmm7psrlwxmm8,1pandxmm8,xmm2psubbxmm7,xmm8movdqaxmm8,xmm7pandxmm8,xmm3psrlwxmm7,2pandxmm7,xmm3paddbxmm7,xmm8movdqaxmm8,xmm7psrlwxmm8,4paddbxmm8,xmm7pandxmm8,xmm5psadbwxmm8,xmm0paddqxmm4,xmm8punpcklbwxmm6,xmm0punpcklwdxmm6,xmm0punpckldqxmm6,xmm0movdqaxmm7,xmm6psrlwxmm7,1pandxmm7,xmm2psubbxmm6,xmm7movdqaxmm7,xmm6pandxmm7,xmm3psrlwxmm6,2pandxmm6,xmm3paddbxmm6,xmm7movdqaxmm7,xmm6psrlwxmm7,4paddbxmm7,xmm6pandxmm7,xmm5psadbwxmm7,xmm0paddqxmm1,xmm7addrax,4cmprcx,raxjne .LBB0_6paddqxmm1,xmm4pshufdxmm0,xmm1,238paddqxmm0,xmm1movqrax,xmm0cmprcx,rsije .LBB0_2.LBB0_8:movzxedx, byte ptr [rdi+rcx]imuledx,edx,134480385shredx,3andedx,286331153imuledx,edx,286331153shredx,28addrax,rdxincrcxcmprsi,rcxjne .LBB0_8.LBB0_2:retfind_any:xorecx,ecx.LBB1_1:movrax,rcxcmprsi,rcxje .LBB1_3learcx,[rax+1]cmp byte ptr [rdi+rax],0je .LBB1_1.LBB1_3:cmprsi,rax setne alretConclusion
|
alamb
commented
Apr 8, 2025
Thanks again for the wonderful work @acking-you -- I also confirmed that without the short circuit optimization the benchmarks get much much slower Let's merge this PR in and I will file a follow on ticket to track potentially improving performance even more |
alamb
left a comment
There was a problem hiding this comment.
Thanks again everyone -- this is a good improvement. Also really nice analysis work by @acking-you
| /// # test bool_or/bool_and | ||
| /// cargo bench --bench binary_op -- boolean_ops | ||
| /// ``` | ||
| fn benchmark_boolean_ops(c: &mut Criterion) { |
There was a problem hiding this comment.
since these are basically arrow benchmarks, I made a small follow on PR to propose removing them for now:
alamb
commented
Apr 8, 2025
the extended tests appear to be failing after this PR: |
* [draft] add shot circuit in BinaryExpr * refactor: add check_short_circuit function * refactor: change if condition to match * feat: Add support for --mem-pool-type and --memory-limit options to multiple benchmarks (apache#14642) * Add support --mem-pool-type and --memory-limit options for all benchmarks * Add --sort-spill-reservation-bytes option * Chore/Add additional FFI unit tests (apache#14802) * Add unit tests to FFI_ExecutionPlan * Add unit tests for FFI table source * Add round trip tests for volatility * Add unit tests for FFI insert op * Simplify string generation in unit test Co-authored-by: Andrew Lamb <andrew@nerdnetworks.org> * Fix drop of borrowed value --------- Co-authored-by: Andrew Lamb <andrew@nerdnetworks.org> * Improve feature flag CI coverage `datafusion` and `datafusion-functions` (apache#15203) * add extend sql & docs * feat: Add support for --mem-pool-type and --memory-limit options to multiple benchmarks (apache#14642) * Add support --mem-pool-type and --memory-limit options for all benchmarks * Add --sort-spill-reservation-bytes option * Chore/Add additional FFI unit tests (apache#14802) * Add unit tests to FFI_ExecutionPlan * Add unit tests for FFI table source * Add round trip tests for volatility * Add unit tests for FFI insert op * Simplify string generation in unit test Co-authored-by: Andrew Lamb <andrew@nerdnetworks.org> * Fix drop of borrowed value --------- Co-authored-by: Andrew Lamb <andrew@nerdnetworks.org> * Improve feature flag CI coverage `datafusion` and `datafusion-functions` (apache#15203) * fix: incorrect false judgment * add test * separate q6 to new PR * feat: Add support for --mem-pool-type and --memory-limit options to multiple benchmarks (apache#14642) * Add support --mem-pool-type and --memory-limit options for all benchmarks * Add --sort-spill-reservation-bytes option * Chore/Add additional FFI unit tests (apache#14802) * Add unit tests to FFI_ExecutionPlan * Add unit tests for FFI table source * Add round trip tests for volatility * Add unit tests for FFI insert op * Simplify string generation in unit test Co-authored-by: Andrew Lamb <andrew@nerdnetworks.org> * Fix drop of borrowed value --------- Co-authored-by: Andrew Lamb <andrew@nerdnetworks.org> * Improve feature flag CI coverage `datafusion` and `datafusion-functions` (apache#15203) * feat: Add support for --mem-pool-type and --memory-limit options to multiple benchmarks (apache#14642) * Add support --mem-pool-type and --memory-limit options for all benchmarks * Add --sort-spill-reservation-bytes option * Chore/Add additional FFI unit tests (apache#14802) * Add unit tests to FFI_ExecutionPlan * Add unit tests for FFI table source * Add round trip tests for volatility * Add unit tests for FFI insert op * Simplify string generation in unit test Co-authored-by: Andrew Lamb <andrew@nerdnetworks.org> * Fix drop of borrowed value --------- Co-authored-by: Andrew Lamb <andrew@nerdnetworks.org> * Improve feature flag CI coverage `datafusion` and `datafusion-functions` (apache#15203) * add benchmark for boolean_op * fix cargo doc * add binary_op bench * Better comments --------- Co-authored-by: Kristin Cowalcijk <bo@wherobots.com> Co-authored-by: Tim Saucer <timsaucer@gmail.com> Co-authored-by: Andrew Lamb <andrew@nerdnetworks.org>

Which issue does this PR close?
BinaryExprevaluate lacks optimization forOrandAndscenarios #11212.Rationale for this change
What changes are included in this PR?
BinaryExprevaluate lacks optimization forOrandAndscenarios #11212 (comment) .)Below is the performance comparison of running the extended SQL locally. It seems there is also some improvement in Q4 (maybe noise).
At the same time, while creating this SQL, I also discovered a bug — one of the filter caused a panic: #15461
Are these changes tested?
Are there any user-facing changes?