Make Mask execution strict about nullable booleans#8121
Conversation
Mask execution now requires a non-nullable boolean array and errors on nullable input. The new MaskNullAsFalse executable preserves the previous null-as-false coercion for filter and pruning predicates over nullable data, where SQL semantics treat NULL as not matching. Predicate-evaluation call sites (filter, prune, dict filter, is_constant) use MaskNullAsFalse; validity-array call sites keep the stricter Mask. Signed-off-by: Claude <noreply@anthropic.com>
Remove the shared NullHandling enum and execute_mask helper in favor of a self-contained Executable impl for each target. Mask still requires a non-nullable boolean array; MaskNullAsFalse coerces nulls to false. Signed-off-by: Claude <noreply@anthropic.com>
Polar Signals Profiling ResultsLatest Run
Previous Runs (2)
Powered by Polar Signals Cloud |
Benchmarks: PolarSignals ProfilingVortex (geomean): 1.009x ➖ How to read Verdict and Engines
datafusion / vortex-file-compressed (1.009x ➖, 0↑ 0↓)
No file size changes detected. |
File Sizes: PolarSignals ProfilingNo file size changes detected. |
Benchmarks: TPC-H SF=1 on NVMEVerdict: No clear signal (low confidence) How to read Verdict and Engines
datafusion / vortex-file-compressed (0.988x ➖, 0↑ 0↓)
datafusion / vortex-compact (0.999x ➖, 0↑ 0↓)
datafusion / parquet (0.998x ➖, 0↑ 0↓)
datafusion / arrow (0.984x ➖, 1↑ 0↓)
duckdb / vortex-file-compressed (0.995x ➖, 0↑ 0↓)
duckdb / vortex-compact (1.003x ➖, 0↑ 0↓)
duckdb / parquet (1.001x ➖, 1↑ 1↓)
duckdb / duckdb (0.994x ➖, 0↑ 0↓)
File Size Changes (11 files changed, +0.1% overall, 6↑ 5↓)
Totals:
|
File Sizes: TPC-H SF=1 on NVMENo file size changes detected. |
Benchmarks: FineWeb NVMeVerdict: No clear signal (low confidence) How to read Verdict and Engines
datafusion / vortex-file-compressed (1.008x ➖, 0↑ 1↓)
datafusion / vortex-compact (0.987x ➖, 1↑ 1↓)
datafusion / parquet (0.969x ➖, 1↑ 0↓)
duckdb / vortex-file-compressed (0.996x ➖, 0↑ 0↓)
duckdb / vortex-compact (0.994x ➖, 0↑ 0↓)
duckdb / parquet (1.009x ➖, 0↑ 0↓)
File Size Changes (1 files changed, +0.1% overall, 1↑ 0↓)
Totals:
|
File Sizes: FineWeb NVMeNo file size changes detected. |
Benchmarks: TPC-DS SF=1 on NVMEVerdict: No clear signal (low confidence) How to read Verdict and Engines
datafusion / vortex-file-compressed (0.996x ➖, 2↑ 1↓)
datafusion / vortex-compact (1.001x ➖, 1↑ 2↓)
datafusion / parquet (0.991x ➖, 2↑ 1↓)
duckdb / vortex-file-compressed (1.004x ➖, 1↑ 3↓)
duckdb / vortex-compact (1.003x ➖, 1↑ 1↓)
duckdb / parquet (0.997x ➖, 1↑ 0↓)
duckdb / duckdb (1.002x ➖, 0↑ 2↓)
File Size Changes (6 files changed, +0.0% overall, 2↑ 4↓)
Totals:
|
File Sizes: TPC-DS SF=1 on NVMENo file size changes detected. |
Benchmarks: FineWeb S3Verdict: No clear signal (environment too noisy confidence) How to read Verdict and Engines
datafusion / vortex-file-compressed (0.898x ➖, 1↑ 1↓)
datafusion / vortex-compact (0.951x ➖, 1↑ 0↓)
datafusion / parquet (0.949x ➖, 0↑ 0↓)
duckdb / vortex-file-compressed (0.896x ➖, 1↑ 0↓)
duckdb / vortex-compact (1.015x ➖, 0↑ 1↓)
duckdb / parquet (0.912x ➖, 0↑ 0↓)
|
Benchmarks: Statistical and Population GeneticsVerdict: No clear signal (low confidence) How to read Verdict and Engines
duckdb / vortex-file-compressed (1.006x ➖, 0↑ 0↓)
duckdb / vortex-compact (0.994x ➖, 0↑ 0↓)
duckdb / parquet (1.003x ➖, 0↑ 0↓)
File Size Changes (1 files changed, +0.0% overall, 1↑ 0↓)
Totals:
|
File Sizes: Statistical and Population GeneticsNo file size changes detected. |
Benchmarks: TPC-H SF=10 on NVMEVerdict: No clear signal (low confidence) How to read Verdict and Engines
datafusion / vortex-file-compressed (1.050x ➖, 0↑ 0↓)
datafusion / vortex-compact (1.011x ➖, 0↑ 0↓)
datafusion / parquet (1.065x ➖, 0↑ 4↓)
datafusion / arrow (1.026x ➖, 0↑ 2↓)
duckdb / vortex-file-compressed (0.993x ➖, 0↑ 0↓)
duckdb / vortex-compact (0.994x ➖, 0↑ 0↓)
duckdb / parquet (0.995x ➖, 0↑ 0↓)
duckdb / duckdb (0.991x ➖, 0↑ 0↓)
File Size Changes (27 files changed, -0.0% overall, 10↑ 17↓)
Totals:
|
File Sizes: TPC-H SF=10 on NVMENo file size changes detected. |
Benchmarks: Clickbench on NVMEVerdict: No clear signal (low confidence) How to read Verdict and Engines
datafusion / vortex-file-compressed (0.990x ➖, 1↑ 0↓)
datafusion / parquet (0.993x ➖, 0↑ 1↓)
duckdb / vortex-file-compressed (0.986x ➖, 2↑ 0↓)
duckdb / parquet (1.002x ➖, 0↑ 1↓)
duckdb / duckdb (1.000x ➖, 0↑ 0↓)
File Size Changes (104 files changed, +0.0% overall, 54↑ 50↓)
Totals:
|
File Sizes: Clickbench on NVMEFile Size Changes (1 files changed, -0.0% overall, 0↑ 1↓)
Totals:
|
Benchmarks: TPC-H SF=1 on S3Verdict: No clear signal (environment too noisy confidence) How to read Verdict and Engines
datafusion / vortex-file-compressed (0.878x ➖, 3↑ 0↓)
datafusion / vortex-compact (0.775x ➖, 8↑ 1↓)
datafusion / parquet (1.047x ➖, 0↑ 1↓)
duckdb / vortex-file-compressed (0.896x ➖, 1↑ 0↓)
duckdb / vortex-compact (0.924x ➖, 0↑ 0↓)
duckdb / parquet (0.895x ➖, 0↑ 0↓)
|
Benchmarks: CompressionVortex (geomean): 0.999x ➖ How to read Verdict and Engines
unknown / unknown (0.990x ➖, 1↑ 0↓)
|
Benchmarks: Random AccessVortex (geomean): 1.010x ➖ How to read Verdict and Engines
unknown / unknown (1.045x ➖, 1↑ 2↓)
|
Benchmarks: TPC-H SF=10 on S3Verdict: No clear signal (environment too noisy confidence) How to read Verdict and Engines
datafusion / vortex-file-compressed (1.021x ➖, 0↑ 1↓)
datafusion / vortex-compact (0.914x ➖, 1↑ 1↓)
datafusion / parquet (0.923x ➖, 1↑ 0↓)
duckdb / vortex-file-compressed (0.996x ➖, 0↑ 0↓)
duckdb / vortex-compact (0.989x ➖, 0↑ 0↓)
duckdb / parquet (0.943x ➖, 0↑ 0↓)
|
…ing-1eKcy Signed-off-by: Joe Isaacs <joe.isaacs@live.co.uk> # Conflicts: # vortex-array/public-api.lock # vortex/public-api.lock
Heap allocates what exactly? Null-as-false is a perfectly valid scalar function. We should just treat it like one. You're over-optimizing if you're trying to avoid wrapping this up in a ScalarFnArray. |
|
This PR has been marked as stale because it has been open for 14 days with no activity. Please comment or remove the stale label if you wish to keep it active, otherwise it will be closed in 7 days |
|
@robert3005 thoughts |
|
I recently optimised in place owned bitbuffer operations such that things like null as false will attempt to reuse the existing buffer where possible. Is there still a difference then? |
|
Yes the semantics are the ONLY important change here |
…ing-1eKcy Signed-off-by: Joe Isaacs <joe.isaacs@live.co.uk> # Conflicts: # vortex-buffer/src/bit/buf.rs # vortex-layout/src/layouts/zoned/zone_map.rs
|
Ok by what are we saving by this not being a scalar function? I think we should change the semantics of mask to require non nullable bool but then... I think I agree with @gatesn, why is this another executable target? |
Remove the NullAsFalse executor adapter and its supporting buffer helpers. Mask::execute keeps rejecting nullable boolean arrays; callers that need null-as-false semantics now call array.fill_null(false) before executing into a Mask. Signed-off-by: Joe Isaacs <joe.isaacs@live.co.uk>
Signed-off-by: Joe Isaacs <joe.isaacs@live.co.uk>
Mask execution strict about nullable booleans
Merging this PR will improve performance by 10.85%
|
| Mode | Benchmark | BASE |
HEAD |
Efficiency | |
|---|---|---|---|---|---|
| ⚡ | Simulation | encode_varbin[(1000, 2)] |
175.3 µs | 158.2 µs | +10.85% |
Tip
Curious why this is faster? Comment @codspeedbot explain why this is faster on this PR, or directly use the CodSpeed MCP with your agent.
Comparing claude/awesome-turing-1eKcy (7306d0d) with develop (739b745)
Footnotes
-
10 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. ↩
Benchmarks: Appian on NVMEVerdict: No clear signal (low confidence) How to read Verdict and Engines
datafusion / vortex-file-compressed (0.999x ➖, 0↑ 0↓)
datafusion / parquet (0.913x ➖, 2↑ 0↓)
duckdb / vortex-file-compressed (1.032x ➖, 0↑ 0↓)
duckdb / parquet (1.004x ➖, 0↑ 0↓)
duckdb / duckdb (0.966x ➖, 0↑ 0↓)
File Size Changes (4 files changed, -0.1% overall, 0↑ 4↓)
Totals:
|
Replace the execute_mask_coercing_nulls helper with an Executor-style `ArrayRef::null_as_false()` adapter (matching the design in #8121's history): canonicalize once and fold validity into the value bits with a single AND that reuses the value buffer when uniquely owned, deferring non-nullable input to the strict `Mask::execute`. Add the mask_fill_null microbenchmark and a one-line note that `fill_null` on a lazy `ScalarFn` array is currently slow. Verified on CI ClickBench: clickbench_q22/duckdb:vortex 1126ms -> 580ms (0.51, ~2x). Signed-off-by: Joe Isaacs <joe.isaacs@live.co.uk> Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Summary
Previously
Mask::executeaccepted nullable boolean arrays and silentlycoerced null elements to
false(folding validity into the value bits). Thatbehaviour was flagged with a
TODOquestioning whether the implicit coercionwas correct.
This PR makes
Maskexecution strict:Mask::executenow requires anon-nullable boolean array (
Bool(NonNullable)) and errors on nullable input.The null-as-
falsecoercion is no longer implicit — callers that wantSQL-style
NULL-as-not-matching semantics opt in explicitly viaarray.fill_null(false)before executing into aMask.