Repository navigation
refactor: move native -0.0 and NaN handling into one float_semantics module - #6400
Conversation
…module Part of apache#6385. Each native expression that follows one of Spark's float rules carried its own copy of that rule. This moves them onto a new `float_semantics` module in `spark-expr`, with no change in behavior. - `normalize_float`, `canonicalize_nan`, `compare_floats` and `hash_input` are the per-value rules: `NormalizeNaNAndZero`, `Double.equals`, `compareDoubles`, and the `Murmur3Hash`/`XxHash64` input. - `normalize_floats`, `normalize_nested_floats` and `has_float_leaf` normalize arrays. `NormalizeNaNAndZero` and `NormalizeNestedFloats` move into the module, and `NormalizeNaNAndZero::wrap_if_needed` takes over the planner's check in `create_normalized_key_expr`. - `spark_comparator` replaces both `nested_equality` (nested `=` and `IN`) and the comparator local to `array_min`/`array_max`. hll_plus_plus, max_min_by, mode, percentile, array_extrema, array_position, arrays_overlap, nested_comparison, the float hash macros, and the planner's sort keys and range partition bounds now call the module.
- compare_floats uses the if-chain form again, so `.is_eq()` in array_position and percentile compiles to a single comparison. - float_extrema calls new float_lt/float_gt predicates, which compile to the same instructions as the inline tests they replace. The three-way comparison was up to 9x slower in that loop. - spark_equality gives nested `=` and `IN` back the length check that skips comparing the elements of lists of different lengths. - float_semantics is a private module again, re-exporting normalize_floats, canonicalize_nan and the two expressions. - The Parquet variant cast canonicalizes NaN with canonicalize_nan, and gains a test. - Tests share the NaN constants and check the per-value rules in one table, and the max_by doc defers to the module docs.
mbutrovich
left a comment
There was a problem hiding this comment.
Thanks @andygrove. Having one place that names the Spark rule each helper follows will make the follow-ups in #6385 much easier to review.
I checked the performance claims on an Apple M5 Max (aarch64, release profile, rustc 1.98.1) against the merge base 8369bf1. The float compare blocks in float_extrema are the same instructions as before, and the float hash loops now use a branchless fcmp/fcsel into a single inlined hash where they used to branch between two hash sites. The float loop in array_position is now unswitched on whether the search value is NaN. The nested_comparison criterion bench for eq and dynamic at widths 1 and 16 moved between -12% and +6% with no consistent direction, which is within noise at that sample size.
…mparator `check_types` compares logical types, so a `Dictionary(_, Float64)` passes as `Float64` and then panicked when `float_comparator` downcast it. The comparator now matches on both sides' types and returns the internal error. `mismatched_types_are_rejected` covers a dictionary on either side, at the top level and inside a list.
mbutrovich
left a comment
There was a problem hiding this comment.
Thanks for the quick revision @andygrove, LGTM!
|
Thanks for the review @mbutrovich |
…, 4.1.4 and 4.2.1 (#6989) (#7265) * fix: treat -0.0 and 0.0 as one value in arrays_overlap on Spark 4.0.5, 4.1.4 and 4.2.1 Spark 4.0.5, 4.1.4 and 4.2.1 normalize each flat FLOAT or DOUBLE element before arrays_overlap looks it up in its hash set of boxed values (SPARK-59602), so -0.0 and 0.0 are one value, as they already were on 4.2.0, which normalizes the arguments in the plan (SPARK-54918). The native kernel kept the zeros apart on every version, so on those releases it returned false where Spark returns true. Register a second variant, spark_arrays_overlap_normalized, whose flat float arms key elements by normalize_float instead of canonicalize_nan, and have CometArraysOverlap pick it wherever ArraySetSupport.normalizesFloats holds. Earlier releases keep the existing kernel. Both variants treat every NaN as one value and compare nested elements in Spark's SQL ordering. Closes #6768. * review: name the variant in arrays_overlap errors and trim the version table Use self.name() in the unsupported-type errors so spark_arrays_overlap_normalized reports itself, and test it on the array and scalar paths. Reduce the native docs to the two equality policies and point at CometArraysOverlap.nativeFunction for the version mapping. Keep one Spark version on each side of the boundary in the routing test and the convert assertion. Adapted for branch-1.1: - branch-1.1 has no ArraySetSupport.normalizesFloats (#6563), so this adds it next to normalizesArgumentsInPlan, for arrays_overlap only. array_distinct and array_union keep branch-1.1's 4.2.0-only check. - arrays_overlap.rs takes normalize_float from math_funcs::internal and keeps branch-1.1's own NaN keying, since branch-1.1 predates the float_semantics module (#6400). - The new test pins the version boundaries itself, since branch-1.1 doesn't have the test that does on main. - The user-guide floating-point page gets the arrays_overlap section only. The contributor-guide pages are dropped, since that guide isn't published from release branches. (cherry picked from commit 3565f2f) Co-authored-by: Andy Grove <agrove@apache.org>
Which issue does this PR close?
Part of #6385. This is the "consolidate the native helpers into the shared module (no behavior change)" step.
Rationale for this change
Spark's
-0.0and NaN rules were re-implemented in each native expression that needs one: three copies ofNormalizeNaNAndZero, three ofcompareDoubles, two nested comparators, and six inline-0.0checks in the hash macros. Putting each rule in one place means the later steps in #6385 each change one function, not every copy. Those steps are canonicalizing NaN inhash/xxhash64, normalizing comparisons, fixingmin/maxand friends, and nested ordering.This PR does not change behavior. The fixes come in the follow-up PRs.
What changes are included in this PR?
A new
native/spark-expr/src/float_semantics/module. Its module docs list which Spark rule each helper implements.normalize_floatNormalizeNaNAndZeromath_funcs/internal/normalize_nan.rs, the closures incanonicalize_float_ordering(max_min_by.rs), themode.rsmacrocanonicalize_nanjava.lang.Double.equalsOverlapKeyforf32/f64(arrays_overlap.rs),modebefore Spark 4.2, the NaN rule in the Parquet variant cast (variant.rs)compare_floatsSQLOrderingUtil.compareDoublesspark_double_cmp(percentile.rs), the inline check inposition_float(array_position.rs)float_lt,float_gtcompareDoubles, as single tests for hot loopsfloat_extrema(array_extrema.rs)hash_inputMurmur3Hash/XxHash64input-0.0checks inhash_funcs/utils.rsnormalize_floatsNormalizeNaNAndZeroon a flat arrayNormalizeNaNAndZero::normalize_array,normalize_floats(hll_plus_plus.rs),canonicalize_float_ordering(max_min_by.rs)normalize_nested_floats,has_float_leaf,NormalizeNestedFloatsNormalizeNaNAndZeroat any deptharray_funcs/nested_float_normalize.rsNormalizeNaNAndZero::wrap_if_neededcreate_normalized_key_expr(planner.rs)spark_comparator,spark_equalitycompareDoublesinside lists and structsspark_comparator(array_extrema.rs) andnested_equality(nested_comparison.rs)Notes for review:
spark_comparatortakes a left and a right array, which the nested ordering work (Comparison operators on arrays and structs with floating-point leaves do not match Spark for signed zero #6157, Match Spark ordering and rank semantics for floating values nested in arrays and structs #5507) needs. Nulls sort first at every level, asarray_min/array_maxrequire.spark_equalityruns the same traversal for nested=/IN, and treats lists of different lengths as unequal without comparing their elements, as the oldnested_equalitydid.hash_inputkeeps today's behavior: it folds-0.0but does not canonicalize NaN. Its doc comment says so and points at [EPIC] Match Spark's -0.0 and NaN semantics systematically instead of per expression #6385, since the hash fix is the next step and now lands in this one function.percentile.rs,array_position.rsandfloat_extremawere not in the issue's list, but each held a copy ofcompareDoubles. The Parquet variant cast held a copy of thedoubleToLongBitsNaN rule.Mapsupport in the nested normalization, and the hash NaN fix.float_semanticsis a private module. The crate root re-exportsnormalize_floatsandcanonicalize_nanfor core, plusNormalizeNaNAndZeroandNormalizeNestedFloatsas before.hash_array_primitive_float!drops its two type parameters, which only fed the inline-0.0check.List: the oldarray_min/array_maxcomparator fell back to Arrow's float order inside aLargeListorFixedSizeList, and the shared comparator uses Spark's order there too. The type-mismatch internal error, which is also unreachable, now reads "Spark comparison requires matching types". A dictionary array compared with a plain float array passes the logical type check. It now returns an internal error, where the oldnested_equalitypanicked.How are these changes tested?
The existing tests pass unchanged. These were run on macOS aarch64 with the default Spark 4.1 profile:
datafusion-comet-spark-exprunit tests: 1022 passed.datafusion-cometunit tests: 552 passed, 5 ignored.CometSqlFileTestSuite: 578 passed. This includes the float fixtures forarray_min/array_max,arrays_overlap,array_position,mode,max_by/min_by,approx_count_distinct,percentile,hashand nestedIN.CometArrayExpressionSuiteandCometHashExpressionSuite: 109 passed.CometWindowExecSuiteandCometNativeShuffleSuite: 124 passed. These cover window partition keys and range partition bounds.New unit tests:
normalize_floatsleaving nested arrays unchanged.wrap_if_needednot wrapping a key twice.percentileNaN-ordering test, replacing the test of the removedspark_double_cmp.I also checked equivalence with a throwaway differential test, not committed, that runs the old helpers verbatim against the new ones:
f32andf64.spark_comparatorandspark_equalityagainst the oldnested_equalityon 2.04M pairs, andspark_comparatoragainst the oldarray_extremacomparator on 1.10M pairs.I also planted six mutations in the new code, and the committed tests catch each one.
Performance matches the old code. I timed old and new copies in a standalone release build on an Apple M3 Max, on 8192-row batches:
float_extremacompiles to the same instructions as before.array_position,percentileand nested=/INare within noise of the old times, including lists of different lengths.