perf: don't use fill_null for executing an array into a mask#8466
Conversation
…sion) #8121 inserted `array.fill_null(false)?` before every `execute::<Mask>` filter call site. `fill_null` is a no-op only for non-nullable arrays; for nullable predicate results (e.g. `LIKE`/`<>` over nullable columns, as ClickBench string columns are stored) it takes the expensive path: `precondition` calls `array.validity()?` on the lazy predicate array, re-deriving validity from the expression tree and materializing an intermediate array, before `execute::<Mask>` canonicalizes again. This regressed ClickBench Q22. Add `execute_mask_coercing_nulls`, which canonicalizes the array once and folds validity into the value bits with a single bitmap AND (the pre-#8121 cost profile), and route the filter readers (dict/flat/partitioned/row_idx/zone_map, CUDA, is_constant) through it. `Mask::execute` stays strict about nullable input. Signed-off-by: Joe Isaacs <joe.isaacs@live.co.uk> Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Merging this PR will not alter performance
|
| Mode | Benchmark | BASE |
HEAD |
Efficiency | |
|---|---|---|---|---|---|
| ❌ | Simulation | chunked_varbinview_canonical_into[(1000, 10)] |
162.2 µs | 198.2 µs | -18.16% |
| ❌ | Simulation | chunked_varbinview_canonical_into[(100, 100)] |
273.2 µs | 308.6 µs | -11.49% |
| ❌ | Simulation | chunked_varbinview_into_canonical[(100, 100)] |
330.8 µs | 367.7 µs | -10.04% |
| ⚡ | Simulation | chunked_varbinview_opt_canonical_into[(1000, 10)] |
213.8 µs | 176.9 µs | +20.86% |
| ⚡ | Simulation | chunked_varbinview_opt_into_canonical[(1000, 10)] |
229.1 µs | 193.4 µs | +18.46% |
Tip
Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.
Comparing ji/fix-mask-fill-null-regression (b3c69d8) with develop (a4476f1)
Footnotes
-
3 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. ↩
Polar Signals Profiling ResultsLatest Run
Powered by Polar Signals Cloud |
Benchmarks: PolarSignals ProfilingVortex (geomean): 1.019x ➖ How to read Verdict and Engines
datafusion / vortex-file-compressed (1.019x ➖, 0↑ 1↓)
No file size changes detected. |
Benchmarks: FineWeb NVMeVerdict: No clear signal (low confidence) How to read Verdict and Engines
datafusion / vortex-file-compressed (1.036x ➖, 0↑ 0↓)
datafusion / vortex-compact (0.987x ➖, 1↑ 1↓)
datafusion / parquet (1.005x ➖, 0↑ 0↓)
duckdb / vortex-file-compressed (0.998x ➖, 1↑ 2↓)
duckdb / vortex-compact (1.012x ➖, 0↑ 0↓)
duckdb / parquet (1.059x ➖, 0↑ 2↓)
File Size Changes (1 files changed, +0.0% overall, 1↑ 0↓)
Totals:
|
Benchmarks: TPC-H SF=1 on NVMEVerdict: No clear signal (environment too noisy confidence) How to read Verdict and Engines
datafusion / vortex-file-compressed (1.004x ➖, 0↑ 0↓)
datafusion / vortex-compact (1.019x ➖, 0↑ 0↓)
datafusion / parquet (1.005x ➖, 0↑ 1↓)
datafusion / arrow (1.004x ➖, 1↑ 0↓)
duckdb / vortex-file-compressed (1.007x ➖, 0↑ 0↓)
duckdb / vortex-compact (1.005x ➖, 0↑ 0↓)
duckdb / parquet (1.007x ➖, 1↑ 2↓)
duckdb / duckdb (1.013x ➖, 0↑ 0↓)
File Size Changes (10 files changed, -0.0% overall, 3↑ 7↓)
Totals:
|
Benchmarks: TPC-DS SF=1 on NVMEVerdict: No clear signal (low confidence) How to read Verdict and Engines
datafusion / vortex-file-compressed (1.021x ➖, 0↑ 4↓)
datafusion / vortex-compact (1.020x ➖, 2↑ 4↓)
datafusion / parquet (1.022x ➖, 1↑ 4↓)
duckdb / vortex-file-compressed (1.014x ➖, 0↑ 2↓)
duckdb / vortex-compact (1.013x ➖, 2↑ 3↓)
duckdb / parquet (1.006x ➖, 1↑ 1↓)
duckdb / duckdb (1.001x ➖, 1↑ 2↓)
File Size Changes (6 files changed, +0.0% overall, 2↑ 4↓)
Totals:
|
Benchmarks: FineWeb S3Verdict: No clear signal (environment too noisy confidence) How to read Verdict and Engines
datafusion / vortex-file-compressed (1.282x ➖, 0↑ 3↓)
datafusion / vortex-compact (0.869x ➖, 1↑ 0↓)
datafusion / parquet (1.002x ➖, 1↑ 0↓)
duckdb / vortex-file-compressed (0.989x ➖, 0↑ 0↓)
duckdb / vortex-compact (0.938x ➖, 1↑ 0↓)
duckdb / parquet (0.961x ➖, 0↑ 0↓)
|
Benchmarks: Statistical and Population GeneticsVerdict: No clear signal (low confidence) How to read Verdict and Engines
duckdb / vortex-file-compressed (1.007x ➖, 0↑ 0↓)
duckdb / vortex-compact (0.998x ➖, 0↑ 0↓)
duckdb / parquet (0.998x ➖, 0↑ 0↓)
File Size Changes (1 files changed, +0.0% overall, 1↑ 0↓)
Totals:
|
Benchmarks: TPC-H SF=10 on NVMEVerdict: No clear signal (low confidence) How to read Verdict and Engines
datafusion / vortex-file-compressed (1.160x ❌, 0↑ 19↓)
datafusion / vortex-compact (1.136x ❌, 0↑ 18↓)
datafusion / parquet (1.064x ➖, 0↑ 4↓)
datafusion / arrow (1.093x ➖, 0↑ 11↓)
duckdb / vortex-file-compressed (1.123x ❌, 0↑ 17↓)
duckdb / vortex-compact (1.102x ❌, 0↑ 13↓)
duckdb / parquet (1.066x ➖, 0↑ 3↓)
duckdb / duckdb (1.088x ➖, 0↑ 9↓)
File Size Changes (26 files changed, -0.0% overall, 13↑ 13↓)
Totals:
|
Benchmarks: Clickbench on NVMEVerdict: No clear signal (low confidence) How to read Verdict and Engines
datafusion / vortex-file-compressed (0.949x ➖, 6↑ 1↓)
datafusion / parquet (0.977x ➖, 2↑ 1↓)
duckdb / vortex-file-compressed (0.968x ➖, 2↑ 0↓)
duckdb / parquet (0.997x ➖, 0↑ 0↓)
duckdb / duckdb (0.956x ➖, 6↑ 0↓)
File Size Changes (105 files changed, -0.0% overall, 48↑ 57↓)
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.858x ➖, 8↑ 4↓)
datafusion / vortex-compact (1.058x ➖, 0↑ 3↓)
datafusion / parquet (1.096x ➖, 0↑ 4↓)
duckdb / vortex-file-compressed (1.082x ➖, 0↑ 1↓)
duckdb / vortex-compact (1.044x ➖, 0↑ 1↓)
duckdb / parquet (1.033x ➖, 0↑ 0↓)
|
Benchmarks: Appian on NVMEVerdict: No clear signal (low confidence) How to read Verdict and Engines
datafusion / vortex-file-compressed (1.141x ❌, 0↑ 6↓)
datafusion / parquet (1.018x ➖, 0↑ 0↓)
duckdb / vortex-file-compressed (1.086x ➖, 0↑ 4↓)
duckdb / parquet (1.075x ➖, 0↑ 0↓)
duckdb / duckdb (1.076x ➖, 0↑ 2↓)
File Size Changes (4 files changed, -0.0% overall, 0↑ 4↓)
Totals:
|
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.072x ➖, 0↑ 4↓)
datafusion / vortex-compact (1.069x ➖, 1↑ 2↓)
datafusion / parquet (0.979x ➖, 1↑ 3↓)
duckdb / vortex-file-compressed (1.017x ➖, 0↑ 0↓)
duckdb / vortex-compact (1.023x ➖, 0↑ 0↓)
duckdb / parquet (0.987x ➖, 0↑ 0↓)
|
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>
Signed-off-by: Joe Isaacs <joe.isaacs@live.co.uk> Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Clippy redundant_clone fired on array.clone() in null_as_false_matches_fill_null_then_mask since fill_null borrows self and the array is reused afterwards. Signed-off-by: Joe Isaacs <joe.isaacs@live.co.uk>
Our current validity for a ScalarFnArray is very bad. Instead execute and merge buffers for now. See clickbench:duckdb:q22
See #8471