[ 
https://issues.apache.org/jira/browse/SPARK-60085?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
 ]

ASF GitHub Bot updated SPARK-60085:
-----------------------------------
    Labels: pull-request-available  (was: )

> Reduce hash clustering for Float/Double keys in constant maps
> -------------------------------------------------------------
>
>                 Key: SPARK-60085
>                 URL: https://issues.apache.org/jira/browse/SPARK-60085
>             Project: Spark
>          Issue Type: Improvement
>          Components: SQL
>    Affects Versions: 4.2.0, 5.0.0
>            Reporter: Varun Bhandary
>            Priority: Major
>              Labels: pull-request-available
>
> h3. Problem and relevance
> A generated lookup into a constant map containing 1,000 Float/Double category 
> IDs can check hundreds of buckets per row. This affects SQL map expressions 
> and TargetEncoder's category mappings under default lookup and 
> code-generation settings. Results remain correct; the problem is avoidable 
> work repeated across input rows.
> Spark precomputes a hash table for constant (foldable) maps with at least 
> 1,000 entries. These maps stay fixed while lookup keys vary per row. 
> [TargetEncoder uses this 
> pattern|https://github.com/apache/spark/blob/0c14861e323174452d42ffb7947d1c1158c39286/mllib/src/main/scala/org/apache/spark/ml/feature/TargetEncoder.scala#L381-L385]
>  to map category IDs to learned values.
> h3. Why it happens
> For 1,000 entries, the table contains 2,048 buckets, selected using the 
> hash's lowest 11 bits. The current Float hash uses its bit representation; 
> Double XOR-folds its 64 bits into 32. For both types, keys {{0.0}} through 
> {{999.0}} all initially select bucket zero. Collision handling stores them in 
> successive slots, creating one long cluster. [Pinned 
> implementation|https://github.com/apache/spark/blob/0c14861e323174452d42ffb7947d1c1158c39286/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/complexTypeExtractors.scala#L558-L610].
> Spark's actual tables require *500.5 bucket checks on average* for uniformly 
> distributed successful lookups. This structural count is independent of 
> timing.
> h3. Evidence and tradeoff
> A local reference candidate using Spark's existing Murmur3 utility reduces 
> that average to *1.442 for Float and 1.418 for Double*. Against master 
> {{0c14861e323174452d42ffb7947d1c1158c39286}}, repeated execution shows a 
> benefit and a cost:
> ||Workload||Rows||Baseline (ms)||Candidate (ms)||Change across paired runs||
> | TargetEncoder, 1,000 categories, successful lookups | 100,000 | 127.32 | 
> 12.01 | 9.69–13.29× faster |
> | Random fractional Double map, 10,000 keys; all misses using keys −1 through 
> −10,000 | 1,000,000 | 29.62 | 38.75 | 25.2–30.8% slower |
> Each variant ran in three fresh JVMs; times are medians of their per-case 
> medians. Each case used five samples after at least two seconds of warmup for 
> TargetEncoder and five for the map, reversing variant and case order in round 
> two. Preparation and fitting were excluded; aggregates were independently 
> checked. Plans and generated code confirmed the path. These are constructed 
> workloads on one M3 Pro/JDK 21 machine, with one worker and a 4 GiB heap.
> The slower miss case does not perform more average probing (1.5332 → 1.5294 
> checks). Extra hash arithmetic and a wider set of visited buckets accompany 
> it; their individual timing contributions are unprofiled. Dense-map unseen 
> keys improve structurally too; this random-map result does not establish a 
> slowdown for unseen TargetEncoder categories. Existing hashing already 
> distributes random map keys well.
> h3. Proposed direction
> Improve Float/Double hash distribution consistently in table construction and 
> generated lookup. Use Murmur3 as a reference and evaluate a cheaper mixer 
> using all 64 Double bits before selecting the implementation. Preserve lookup 
> semantics, signed-zero equality, NaN handling, other key types and strategy 
> selection. No public APIs, settings or dependencies change.
> This addresses a distribution problem in [SPARK-55959's hash 
> lookup|https://issues.apache.org/jira/browse/SPARK-55959]. The adjacent 
> [null-key work in PR #58875|https://github.com/apache/spark/pull/58875] and 
> TargetEncoder's repeated lookup are separate concerns.
> h3. Reproduction
> With default lookup/codegen settings:
> {code:sql}
> SET spark.sql.adaptive.enabled=false;
> SET spark.sql.shuffle.partitions=1;
> SELECT sum(element_at(
>   map_from_arrays(
>     CAST(sequence(0, 999) AS ARRAY<DOUBLE>),
>     sequence(0, 999)),
>   CAST(id % 1000 AS DOUBLE))) AS checksum
> FROM range(100000);
> {code}
> Expected checksum: {{49950000}}. Replace both {{DOUBLE}} casts with {{FLOAT}} 
> for that case. Prepend {{EXPLAIN CODEGEN}} to the {{SELECT}} to inspect the 
> bucket loop. This demonstrates the affected path; the timings above require 
> the separate benchmark harness.



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

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

Reply via email to