Repository navigation
Ranked backlog: native scalar expressions with per-row optimization opportunities #4942
Copy link
Copy link
Open
Labels
enhancementNew feature or requestNew feature or requestgood first issueGood for newcomersGood for newcomersperformance
Description
Follow-up to #4936. A scan of the native scalar expression implementations in datafusion-comet-spark-expr (native/spark-expr/) for per-row antipatterns: per-element builder loops that could be vectorized Arrow kernels, iter().map().collect() over Option/Result, per-row heap allocation (String/format!/to_string), per-row value(i) allocation, per-element MutableArrayData::extend, and constants recomputed inside the per-row loop.
Each is a small self-contained change. Methodology (benchmark-first, bit-identical, no-regression gate): see the Optimizing Scalar Expressions guide (PR #4933). Ranked by potential impact = per-row cost severity x usage frequency.
High impact (decimal aggregation hot path, common in TPC-DS)
-
spark_unscaled_value(math_funcs/internal/unscaled_value.rs:37) - per-elementInt64Builderloop casting Decimal128 -> Int64, rebuilding an identical null buffer. Fix:arr.unary::<_, Int64Type>(|v| v as i64). Injected by Spark'sDecimalAggregatesrule forsum/avgover narrow decimals. -
spark_make_decimal(math_funcs/internal/make_decimal.rs:45) - per-elementDecimal128Builderloop, Int64 -> Decimal128. Fix:unary_opt(long_to_decimalalready returnsOption). Paired withUnscaledValueon the output side of the same rewrite.
Medium impact
-
list_extract(array_funcs/list_extract.rs:285) - per-rowMutableArrayData::extend(0, start+i, start+i+1)(one element/row) plus aBox<dyn Fn>index adjuster called per row. Fix: build a gather-indexUInt32Arrayand issue onearrow::compute::take; monomorphize the index adjustment. BacksGetArrayItem(arr[i]) andelement_at. -
cast_int_to_timestamp(conversion_funcs/numeric.rs:1006) - per-elementTimestampMicrosecondBuilderloop over an infallible(v as i64) * MICROS_PER_SECOND. Fix:unary+with_timezone_opt. -
read_side_padding(rpad/CHAR(n)) (static_invoke/char_varchar_utils/read_side_padding.rs:213,275) - per-rowchars().count()full scan (twice on the truncate branch) and char-by-charpushfor padding. Fix: scan length once; bulk-fill the pad run (extend/push_strof a cached pad slice). Runs on CHAR(n) columns. -
from_json/json_string_to_struct(json_funcs/from_json.rs:128) - allocates a fullserde_json::Valuetree per row just to read named fields, plusvalue.to_string()for non-string fields. Fix: arrow-json's streaming decoder (arrow_json::ReaderBuilder) or a borrowing visitor. Common in JSON ETL (not TPC-DS). -
NegativeExprANSI overflow check (math_funcs/negative.rs:62) - an extra full per-row scan forMINbefore the (already vectorized) negation, in ANSI mode. Fix:eq_scalar(array, MIN)+anyin one pass. -
spark_cast_decimal_to_boolean(conversion_funcs/numeric.rs:790) - per-elementBooleanBuilderloop (!v.is_zero()). Fix:neqagainst a zero Decimal128 scalar, or single-passiter().map().collect::<BooleanArray>(). -
cast_float_to_timestamp(conversion_funcs/numeric.rs:1056) - per-element builder loop with NaN/inf/overflow checks. Fix:try_unary(ANSI) /unary_opt(Legacy/Try). -
cast_decimal_to_timestamp(conversion_funcs/numeric.rs:1030) - per-element builder loop;scale_factoralready hoisted. Fix:unary(transform is infallible).
Low impact
-
cast_boolean_to_timestamp(conversion_funcs/boolean.rs:60) - per-element builder loop (bool -> 0/1). Fix: single-passiter().map().collect(). -
cast_whole_num_to_binary(conversion_funcs/numeric.rs:213) - per-elementBinaryBuilderloop; binary building is inherently element-wise, but the null branch can be dropped whennull_count() == 0. -
date_trunc/timestamp_truncarray-format variants (kernels/temporal.rs:435,783) - per-rowformat.value(i).to_uppercase()allocation + per-row format->fn string match. Fix:eq_ignore_ascii_case(no allocation); resolve the trunc fn once per distinct format key. Only the array-of-formats path; the literal-format scalar path is already fast. -
array_positionnested-type fallback (array_funcs/array_position.rs:264) - per-elementScalarValue::try_from_arrayallocation. Only triggered for nested/struct element types (primitives/strings/bools already have flat fast paths). -
json_array_length(json_funcs/json_array_length.rs:114) - near-optimal (streaming counter); minor once-per-scalarclone.
Notes
- Already optimized or in flight (excluded from the list): the decimal/int/float casts and narrowing int casts (PRs perf: optimize
spark_array_compact(no-op fast path for null-free arrays) #4934, perf: optimizeCheckOverflowwith a shared no-overflow fast path (ANSI and non-ANSI) #4937-perf: vectorize float/decimal to narrow-integer casts #4941), plus the earlier campaign (spark_cast_int_to_int,to_json,parse_url,spark_size,spark_unhex,substring,date_truncscalar path,spark_arrays_overlap,arrays_zip,get_json_object,regexp_extract/_all,spark_lpad, casts to string, etc.). - Aggregate functions (
agg_funcs/) were out of scope for this scan. - Confirmed NOT candidates (already vectorized or inherently scalar):
contains,split,map_sort,flatten,get_array_struct_fields,normalize_nan,spark_decimal_div,spark_modulo,WideDecimalBinaryExpr, murmur3/xxhash64 hashing, string-parse casts (utf8 -> int/float/decimal/timestamp),make_date/make_time, andrlike/rand(no Arrow kernel applies). AllRegex::newcalls are already hoisted intoLazyLockstatics.
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
enhancementNew feature or requestNew feature or requestgood first issueGood for newcomersGood for newcomersperformance
Activity
perf: vectorize `spark_unscaled_value` (9x faster)
perf: vectorize `spark_unscaled_value` (9x faster)
perf: vectorize `spark_unscaled_value` (9x faster) (#4972)
perf: vectorize `spark_unscaled_value` (9x faster) (apache#4972)
andygrove commentedon Oct 13, 2026
Removing this from the 1.2.0 milestone and unassigning myself so that someone from the community can pick it up. Contributions welcome.