Reveal Arrow exporter fast-path encodings before conversion#8935
Reveal Arrow exporter fast-path encodings before conversion#8935mhk197 wants to merge 2 commits into
Conversation
Polar Signals Profiling ResultsLatest Run
Previous Runs (2)
Powered by Polar Signals Cloud |
Benchmarks: PolarSignals Profiling 📖Vortex (geomean): 0.924x ➖ How to read Verdict and Engines
datafusion / vortex-file-compressed (0.924x ➖, 2↑ 0↓)
No file size changes detected. |
Benchmarks: TPC-H SF=1 on NVME 📖Verdict: No clear signal (environment too noisy confidence) How to read Verdict and Engines
datafusion / vortex-file-compressed (1.001x ➖, 0↑ 0↓)
datafusion / parquet (1.009x ➖, 2↑ 2↓)
datafusion / arrow (0.992x ➖, 1↑ 0↓)
duckdb / vortex-file-compressed (1.010x ➖, 0↑ 0↓)
duckdb / parquet (0.993x ➖, 1↑ 2↓)
File Size Changes (10 files changed, -43.9% overall, 0↑ 10↓)
Totals:
|
Benchmarks: FineWeb NVMe 📖Verdict: No clear signal (low confidence) How to read Verdict and Engines
datafusion / vortex-file-compressed (0.899x ✅, 4↑ 0↓)
datafusion / parquet (0.922x ➖, 2↑ 0↓)
duckdb / vortex-file-compressed (0.877x ✅, 6↑ 0↓)
duckdb / parquet (0.947x ➖, 1↑ 0↓)
File Size Changes (2 files changed, -46.3% overall, 0↑ 2↓)
Totals:
|
Benchmarks: TPC-DS SF=1 on NVME 📖Verdict: No clear signal (low confidence) How to read Verdict and Engines
datafusion / vortex-file-compressed (0.954x ➖, 6↑ 1↓)
datafusion / parquet (0.953x ➖, 9↑ 1↓)
duckdb / vortex-file-compressed (0.963x ➖, 2↑ 0↓)
duckdb / parquet (0.983x ➖, 1↑ 0↓)
File Size Changes (25 files changed, -43.5% overall, 0↑ 25↓)
Totals:
|
Benchmarks: Clickbench Sorted on NVME 📖Verdict: No clear signal (low confidence) How to read Verdict and Engines
datafusion / vortex-file-compressed (0.997x ➖, 1↑ 0↓)
datafusion / parquet (0.964x ➖, 1↑ 0↓)
duckdb / vortex-file-compressed (0.975x ➖, 1↑ 0↓)
duckdb / parquet (0.996x ➖, 0↑ 0↓)
File Size Changes (201 files changed, -42.7% overall, 60↑ 141↓)
Totals:
|
Benchmarks: FineWeb S3 📖Verdict: No clear signal (low confidence) How to read Verdict and Engines
datafusion / vortex-file-compressed (1.084x ➖, 0↑ 1↓)
datafusion / parquet (0.947x ➖, 0↑ 0↓)
duckdb / vortex-file-compressed (0.981x ➖, 0↑ 0↓)
duckdb / parquet (0.965x ➖, 0↑ 0↓)
|
Benchmarks: Statistical and Population Genetics 📖Verdict: No clear signal (low confidence) How to read Verdict and Engines
duckdb / vortex-file-compressed (1.084x ➖, 0↑ 3↓)
duckdb / parquet (1.064x ➖, 0↑ 1↓)
File Size Changes (2 files changed, -32.3% overall, 0↑ 2↓)
Totals:
|
Benchmarks: TPC-H SF=10 on NVME 📖Verdict: No clear signal (low confidence) How to read Verdict and Engines
datafusion / vortex-file-compressed (0.918x ➖, 3↑ 0↓)
datafusion / parquet (0.901x ➖, 13↑ 0↓)
datafusion / arrow (0.983x ➖, 3↑ 0↓)
duckdb / vortex-file-compressed (0.984x ➖, 3↑ 0↓)
duckdb / parquet (0.996x ➖, 0↑ 0↓)
File Size Changes (25 files changed, -44.0% overall, 0↑ 25↓)
Totals:
|
Benchmarks: Clickbench on NVME 📖Verdict: No clear signal (low confidence) How to read Verdict and Engines
datafusion / vortex-file-compressed (0.993x ➖, 0↑ 0↓)
datafusion / parquet (0.993x ➖, 0↑ 0↓)
duckdb / vortex-file-compressed (0.988x ➖, 4↑ 3↓)
duckdb / parquet (0.994x ➖, 1↑ 0↓)
File Size Changes (101 files changed, -39.2% overall, 0↑ 101↓)
Totals:
|
Benchmarks: TPC-H SF=1 on S3 📖Verdict: No clear signal (environment too noisy confidence) How to read Verdict and Engines
datafusion / vortex-file-compressed (0.955x ➖, 1↑ 1↓)
datafusion / parquet (0.961x ➖, 0↑ 1↓)
duckdb / vortex-file-compressed (0.942x ➖, 0↑ 0↓)
duckdb / parquet (0.953x ➖, 0↑ 0↓)
|
|
I think this is very weird approach to the problem. IMHO The datafusion export is not the arrow export. For arrow export we can use ListView but for Datafusion we don't because support is subpar (maybe with 54 it's not?). We then build a datafusion exporter just like a duckdb exporter. The exporter logic encodes the encodings that are desired. |
b158d1e to
246ea4e
Compare
Merging this PR will improve performance by 32.68%
Performance Changes
Tip Curious why this is faster? Comment Comparing Footnotes
|
…te kernels Signed-off-by: Matt Katz <mhkatz97@gmail.com>
Signed-off-by: Matt Katz <mhkatz97@gmail.com>
382f372 to
38e63ff
Compare
Problem
Every Arrow exporter has fast paths for non-canonical encodings that are byte-compatible with its Arrow target —
List(zero-copy offsets),VarBin,Dict,RunEnd,Chunked,Constant— but probes for them with a one-shot downcast. Whenever a wrapper sits on top (a lazymaskscalar-fn,Slice,Filter, ...), the downcast misses and the exporter falls back to full canonicalization followed by a re-encode:VarBin→ decode toVarBinView(build a view per string), thenarrow_castback to offset-basedUtf8/Binary(copy every byte again)Dict→ decode the whole column flat, thenarrow_castre-dictionary-encodes it by hashing every valueRunEnd→ decode flat, then re-detect runsList→ canonicalize toListView, then rebuild offsets/sizes for exportScans produce exactly these shapes whenever a filter is not fully pushed down, so the cost lands on any engine boundary that uses the exporters (DataFusion, Python, JNI/FFI).
Change
Two pieces, both using existing machinery:
1. Exporters execute until a fast-path encoding surfaces. Each exporter defines a small private
Matcherfor the encodings it converts directly (List | Chunked,VarBin,Dict | Constant,RunEnd | Constant,Struct | Chunked | pack) and runsbefore its dispatch, which is otherwise unchanged. Execution stops the moment the matcher hits at the root (or the array is canonical, in which case the existing canonical fallback runs). This replaces the one-shot
execute::<ArrayRef>step thatto_arrow_run_endalready had — that step could only unwrap a single wrapper.2. Register the missing mask/slice execute-parent kernels for
List,VarBin, andDict. These encodings had mask/slice pushdowns implemented only as reduce rules, which run at construction time (.optimize()) and are never re-attempted during execution — and at construction they frequently bail because the mask child is still an unevaluated predicate.ChunkedandRunEndalready register the execute-kernel variants (MaskExecuteAdaptor/SliceExecuteAdaptor,RunEndSliceKernel), which is why their wrappers peel under execution. This PR addsMaskKernel/SliceKernelimpls (delegations to the existingMaskReduce/SliceReduceimpls) and registrations for the remaining three, following theChunkedprecedent. Everyexecute_untilcaller benefits, not just export.One refinement over a plain delegation:
Dict's mask kernel keepsPrimitivecodes probe-able by ANDing the mask into their validity (same buffer), instead of wrapping the codes in a scalar-fn like the reduce rule does — a scalar-fn wrapper would defeat downstream fast paths that downcast the codes (constant-code slicing, specialized primitive slicing).Benchmarks
PolarSignals sf1, DataFusion, medians of 5 iterations, same machine and same data files,
VORTEX_EXPERIMENTAL_PATCHED_ARRAY=1 FLAT_LAYOUT_INLINE_ARRAY_NODE=1, baseline develop @11b5fe873.On the standard (non-ListLayout) file the change is neutral (geomean 0.97, heavy queries within ±3%). An earlier revision of this PR ran
optimize_ctxper exported batch and showed +2–4% drift on the CI suites; that call is gone.The stress case for the exporters is
VORTEX_EXPERIMENTAL_LIST_LAYOUT=1, where the scan hands the exporters wrappedList-encoded columns on every batch (the PolarSignals regression seen in the #8809 benchmark comments). On that file, both binaries reading identical data:The ListLayout on/off penalty drops from 1.63x on develop to 1.28x with this PR (#8889 alone does not move it). The residual is concentrated in a fixed ~1ms overhead on the small queries and q00 (1.7x), which looks reader-side rather than export-side; worth a separate investigation.
Tests
Six new exporter tests wrap fast-path encodings in the lazy wrappers a scan produces and assert correctness plus zero-copy conversion via buffer-pointer identity. Four of them fail without this change (verified by removing the
reveal_for_exportcalls, and separately by running without the kernel registrations):slice_wrapped_list_exports_via_list_fast_path(offsets buffer copied on old path)mask_wrapped_varbin_exports_via_varbin_fast_path(data buffer copied twice on old path)mask_wrapped_dict_exports_via_dict_fast_path(dictionary re-encoded on old path)nested_wrappers_reveal_run_end_fast_path(values buffer materialized on old path)mask_wrapped_list_exports_via_list_fast_pathandmask_wrapped_struct_exports_via_struct_fast_pathare behavior coverage.Checks
cargo nextest run -p vortex-arrow(206 passed)cargo nextest run -p vortex-array(3041 passed)cargo clippy -p vortex-array -p vortex-arrow --all-targets(clean)cargo +nightly fmt --allcargo clippy --all-targets --all-features