andygrove opened a new issue, #5396:
URL: https://github.com/apache/datafusion-comet/issues/5396

   ## Background
   
   Follow-up to #5363. The Scala end-to-end microbenchmarks conflate expression 
cost with scan and columnar-to-row transfer, so a Rust-side Criterion bench is 
the right tool for actually attributing per-row time to an expression. We 
already have a bench harness under `native/spark-expr/benches/`, but coverage 
is ad hoc.
   
   This issue is the catalog: what native expressions exist, which have 
Criterion benches, which don't, and a proposal for filling the gap deliberately 
(not one bench per one expression — there are hundreds of thin wrappers around 
identical shapes).
   
   ## Scope: what counts as a "native expression"
   
   Two sources reach native code from Comet:
   
   1. **Comet-owned kernels** in `native/spark-expr/src/`. These have their own 
Rust implementations that Comet is responsible for.
   2. **DataFusion / datafusion-spark passthroughs** dispatched by function 
name via `CometScalarFunction(...)` in 
`spark/src/main/scala/org/apache/comet/serde/QueryPlanSerde.scala`. Comet's 
responsibility here is limited to argument massaging and correctness gating — 
the kernel itself is upstream.
   
   For (2), a bench inside Comet's tree is redundant with upstream benches for 
the kernel itself, but *is* useful when Comet wraps the passthrough with 
pre/post processing (input coercion, timezone handling, NULL semantics) or when 
the compat gate (`allow_incompat`) selects a different path than the plain DF 
call. The catalog below treats those distinctly.
   
   ## Existing Criterion benches
   
   40 bench files under `native/spark-expr/benches/`:
   
   `aggregate`, `array_size`, `arrays_overlap`, `base64`, `bloom_filter_agg`, 
`cast_binary_to_string`, `cast_decimal_to_string`, `cast_float_to_decimal`, 
`cast_float_to_string`, `cast_from_boolean`, `cast_from_string`, 
`cast_int_to_decimal`, `cast_int_to_timestamp`, 
`cast_non_int_numeric_timestamp`, `cast_numeric`, `cast_string_to_date`, 
`cast_string_to_timestamp`, `ceil`, `check_overflow`, `checked_arithmetic`, 
`conditional`, `date_trunc`, `date_trunc_array_fmt`, `decimal_div`, 
`decimal_rescale`, `floor`, `get_json_object`, `make_decimal`, `map_sort`, 
`normalize_nan`, `padding`, `parse_url`, `regexp_extract`, `round`, `to_csv`, 
`to_json`, `to_time`, `unhex`, `unscaled_value`, `wide_decimal`
   
   Plus `native/core/benches/array_element_append.rs` and `perf.rs`.
   
   ## Coverage matrix — Comet-owned kernels
   
   Categories mirror the directory layout under `native/spark-expr/src/`. `[x]` 
= bench exists, `[ ]` = gap. Passthrough entries are shown separately at the 
end.
   
   ### `agg_funcs`
   
   - [x] `avg_decimal` — covered by `aggregate.rs`
   - [x] `sum_decimal` — covered by `aggregate.rs`
   - [ ] `sum_int`
   - [ ] `avg` (non-decimal)
   - [ ] `approx_percentile`
   - [ ] `percentile`
   - [ ] `correlation`
   - [ ] `covariance` (pop, samp)
   - [ ] `variance` (pop, samp) / `stddev` (pop, samp) — the Welford path
   - [ ] `hll_plus_plus`
   - [x] `bloom_filter_agg`
   - [ ] `first`, `last`
   - [ ] `collect_list`, `collect_set`
   - [ ] `bit_and_agg`, `bit_or_agg`, `bit_xor_agg`
   
   ### `array_funcs`
   
   - [x] `arrays_overlap`
   - [x] `array_size` (`size`)
   - [ ] `array_insert`
   - [ ] `array_position`
   - [ ] `array_slice`
   - [ ] `arrays_zip`
   - [ ] `flatten`
   - [ ] `get_array_struct_fields`
   - [ ] `list_extract`
   - [ ] `sort_array`
   - [ ] `array_join`, `array_max`, `array_min`, `array_remove`, 
`array_intersect`, `array_union`, `array_except`, `array_contains`, 
`array_transform`, `array_exists`, `array_forall`, `array_aggregate`, 
`array_sort`, `zip_with`, `sequence`, `shuffle`, `create_array`, 
`get_array_item`, `element_at` (all reachable via serde entries in 
`arrayExpressions`)
   
   ### `conditional_funcs`
   
   - [x] `if_expr` — covered by `conditional.rs`
   - [x] `case_when` — covered by `conditional.rs`
   
   ### `conversion_funcs` (cast)
   
   Well-covered already:
   
   - [x] `cast_numeric`, `cast_from_string`, `cast_from_boolean`, 
`cast_int_to_decimal`, `cast_float_to_decimal`, `cast_int_to_timestamp`, 
`cast_string_to_date`, `cast_string_to_timestamp`, 
`cast_non_int_numeric_timestamp`, `cast_float_to_string`, 
`cast_binary_to_string`, `cast_decimal_to_string`
   
   Gaps:
   
   - [ ] `cast_string_to_numeric` (there is `cast_from_string` but the 
string-to-numeric path should be exercised across all target widths)
   - [ ] `cast_timestamp_to_string`, `cast_date_to_string`
   - [ ] `cast_timestamp_to_date`, `cast_date_to_timestamp`
   - [ ] `cast_decimal_to_decimal` (rescale is covered, but full cast between 
decimal widths is not)
   - [ ] `trim` variants that route through `conversion_funcs/trim.rs` 
(leading/trailing/both, custom trim strings) — passthrough today, but Comet's 
serde does the coercion
   
   ### `csv_funcs`
   
   - [x] `to_csv`
   - [ ] `csv_to_structs` (from_csv) — the more expensive direction, unbenched
   
   ### `datetime_funcs`
   
   - [x] `date_trunc`, `date_trunc_array_fmt`, `to_time`
   - [ ] `timestamp_trunc`
   - [ ] `unix_timestamp` (timestamp input, date input)
   - [ ] `from_unix_time`
   - [ ] `date_add`, `date_sub`, `date_diff`
   - [ ] `date_from_unix_date`, `unix_date`
   - [ ] `make_date`, `make_time`, `make_interval`, `make_timestamp`, 
`make_ym_interval`, `make_dt_interval`, `multiply_dt_interval`
   - [ ] `hours` / `hour` / `minute` / `second` / `day_of_month` / 
`day_of_week` / `day_of_year` / `week_of_year` / `week_day` / `quarter` / 
`year` / `month` — most are thin, but `extract_date_part.rs` centralizes them 
and is worth one parameterized bench across the field list
   - [ ] `next_day`, `last_day`, `add_months`, `months_between`
   - [ ] `from_utc_timestamp`, `to_utc_timestamp`, `convert_timezone` — 
timezone handling has been a repeat perf hotspot
   - [ ] `seconds_to_timestamp`, `micros_to_timestamp`, `millis_to_timestamp`
   - [ ] `timestamp_add`, `timestamp_diff`
   
   ### `hash_funcs`
   
   - [ ] `murmur3` — used per row on every hash-partitioned shuffle key; 
regressions here hit shuffle throughput directly
   - [ ] `xxhash64`
   
   ### `json_funcs`
   
   - [x] `to_json`, `get_json_object`
   - [ ] `from_json`
   - [ ] `json_array_length`
   - [ ] `json_object_keys`, `schema_of_json`
   
   ### `map_funcs`
   
   - [x] `map_sort`
   - [ ] `map_extract` (`get_map_value`), `map_keys`, `map_values`, 
`map_entries`, `map_from_arrays`, `map_from_entries`, `map_concat`, 
`str_to_map`, `map_filter`, `transform_keys`, `transform_values`, 
`map_zip_with`, `create_map`
   
   ### `math_funcs`
   
   - [x] `ceil`, `floor`, `round`, `unhex`, `checked_arithmetic`, 
`decimal_div`, `decimal_rescale`, `unscaled_value`, `wide_decimal`, 
`check_overflow`, `normalize_nan`, `make_decimal`
   - [ ] `abs`
   - [ ] `log`, `log10`, `log2`, `log1p`, `logarithm`
   - [ ] `pow`, `hypot`
   - [ ] `modulo` (`pmod`, `remainder`)
   - [ ] `negative` / `unary_minus`
   - [ ] `width_bucket`
   - [ ] `conv`, `hex`
   - [ ] `bround`
   - [ ] `nanvl`
   
   ### `nondetermenistic_funcs`
   
   Timing is meaningful for the RNG path even though the values are random:
   
   - [ ] `rand`, `randn`, `rand_str`
   - [ ] `uuid`
   - [ ] `bernoulli_cell_sampler`, `shuffle`
   - [ ] `monotonically_increasing_id`
   
   ### `predicate_funcs`
   
   - [ ] `is_nan`
   - [ ] `rlike` — critical because it has native Rust and JVM fallback modes; 
regression here shows up in every LIKE-heavy query
   
   ### `string_funcs`
   
   - [x] `base64`, `regexp_extract`, `padding` (lpad/rpad)
   - [ ] `unbase64` — has its own kernel
   - [ ] `contains`, `starts_with`, `ends_with`
   - [ ] `length`, `octet_length`, `bit_length` (thin, but common)
   - [ ] `levenshtein`
   - [ ] `regexp_extract_all`, `regexp_in_str`, `regexp_replace`
   - [ ] `split`
   - [ ] `upper`, `lower`, `init_cap` — these are `allow_incompat` gated; the 
two paths (built-in ICU vs Comet's) should both be benched
   - [ ] `substring`, `substring_index`, `left`, `right`
   - [ ] `overlay`
   - [ ] `mask`, `sound_ex`, `format_number`, `format_string`
   - [ ] `find_in_set`, `string_locate`
   - [ ] `elt`
   - [ ] `to_number`, `try_to_number`, `to_character`
   - [ ] `reverse` (string variant; array variant separately)
   - [ ] `empty2null`
   
   ### `struct_funcs`
   
   - [ ] `create_named_struct`
   - [ ] `get_struct_field`
   
   ### `url_funcs`
   
   - [x] `parse_url`
   
   ### Misc / `static_invoke` / kernels
   
   - [ ] `xpath` family (`XPathBoolean`, `XPathShort`, `XPathInt`, `XPathLong`, 
`XPathFloat`, `XPathDouble`, `XPathString`, `XPathList`) — 8 entries, likely 
one parameterized bench
   - [ ] `bloom_filter_might_contain` (probe side; agg side is covered)
   - [ ] `char_varchar_utils` (static_invoke) — pad/trim for CHAR/VARCHAR 
semantics
   
   ## Coverage matrix — DataFusion passthroughs
   
   40 expressions dispatched by name via `CometScalarFunction`. The kernel 
itself is upstream. Comet-owned coverage here should focus on the passthroughs 
whose serde does non-trivial work or where Comet has a compat gate. Everything 
else can be left to upstream benches.
   
   Passthroughs listed for completeness (from `QueryPlanSerde.scala`):
   
   - **Math (trig / transcendentals):** `acos`, `acosh`, `asin`, `asinh`, 
`atan`, `atanh`, `cbrt`, `cos`, `cosh`, `cot`, `csc`, `degrees`, `exp`, 
`expm1`, `factorial`, `greatest`, `least`, `pi`, `radians`, `rint`, `sec`, 
`signum`, `sin`, `sinh`, `sqrt`, `tan`, `tanh`, `bin`. **Skip individually.** 
Add one parameterized "trig fastpath" bench that runs the whole set on Float64 
with and without nulls, so any regression across the whole family surfaces at 
once.
   - **Hash:** `crc32`, `md5`. **Bench both.** Small kernels but on the hot 
path for many workloads.
   - **Strings:** `ascii`, `char`, `instr`, `space`, `trim`, `ltrim`, `rtrim`. 
**Bench `trim` variants** (Comet's serde does whitespace / custom-trim-char 
routing); others can go in the parameterized string-fastpath bench.
   - **Arrays:** `array_distinct`, `array_repeat`. **Bench both** — non-trivial 
per-row work.
   - **Bitwise:** `shiftrightunsigned`. Skip; trivial.
   
   ## Proposal
   
   **Not** one bench per expression. Concretely:
   
   1. **A shared harness** — `native/spark-expr/benches/common.rs` (file 
already exists but is empty) with helpers for building `RecordBatch`es with 
deterministic seeded data at a standard set of row counts (e.g. 8k / 64k / 
512k) and typical null ratios (0%, 10%, 100%). Every new bench uses those 
builders. Fixes the reproducibility problem the Scala benches have.
   
   2. **Parameterized "family" benches** for expressions that share a shape. 
Trig fastpath is one. Extract-date-part fields is another (14 date-part 
accessors → one bench with `BenchmarkId` per field). String-fastpath (`ascii`, 
`chr`, `space`, `length`, `octet_length`, `bit_length`) is another. Cuts total 
bench code by ~5×.
   
   3. **Individual benches** for the expressions listed with `[ ]` above where 
perf is non-trivial, has a compat gate, or has been a historical hotspot. 
Prioritize in this order:
      - **P0 (hot path for typical Spark workloads):** `murmur3`, `xxhash64`, 
`abs`, `upper`/`lower`/`init_cap` (both compat modes), 
`contains`/`starts_with`/`ends_with`, `regexp_replace`, `substring`, timezone 
conversions (`from_utc_timestamp`, `to_utc_timestamp`, `convert_timezone`), 
`unix_timestamp`, `timestamp_trunc`, `is_nan`, `rlike` (both native and JVM 
modes), the remaining aggregates in `agg_funcs`.
      - **P1 (correctness-gated / has known variants):** `from_json`, 
`csv_to_structs`, cast timestamp/date crossings, decimal-to-decimal cast, 
`unbase64`, `mask`, `to_number` / `try_to_number`.
      - **P2 (thin wrappers):** everything else — most `array_funcs`, 
`map_funcs`, `struct_funcs`, `xpath` (one parameterized bench for the family), 
`format_number`/`format_string`, `sound_ex`, `find_in_set`, `string_locate`, 
`elt`, `reverse`, `empty2null`, misc math.
   
   4. **A coverage script** in CI — `native/spark-expr/benches/coverage.sh` (or 
a small Rust test) that reads the list of expression classes registered in 
`QueryPlanSerde.scala` and asserts each one either has a bench file, appears in 
a parameterized-family allowlist, or is on an explicit skip-list with a reason. 
This is what prevents the gap from reopening. Failing this gate should be a 
merge blocker.
   
   5. **A README section** in `native/spark-expr/benches/` documenting the 
convention (harness, naming, parameterization rules, skip-list) so contributors 
adding a new expression know a bench is expected.
   
   Rough size estimate: ~40 new bench files needed to cover the P0+P1 list, if 
each is ~150 lines and reuses the shared harness. That is one focused sprint of 
work, or drip-fed alongside the individual expression changes as they come up. 
The coverage script is what makes the drip-feed viable.
   
   Explicitly out of scope: the Scala end-to-end microbenchmarks. Those have 
separate problems (see #5363) and Criterion benches don't replace them because 
they don't exercise the JNI or serde boundary.
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to