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

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

> Remove redundant second modulo in integral MathUtils.pmod
> ---------------------------------------------------------
>
>                 Key: SPARK-59696
>                 URL: https://issues.apache.org/jira/browse/SPARK-59696
>             Project: Spark
>          Issue Type: Improvement
>          Components: SQL
>    Affects Versions: 4.1.0
>            Reporter: David Mollitor
>            Priority: Minor
>              Labels: pull-request-available
>
> h3. Problem
> The integral {{MathUtils.pmod}} overloads ({{Int}}, {{Long}}, {{Byte}}, 
> {{Short}}) compute the positive remainder as:
> {code:scala}
> val r = a % n
> if (r < 0) (r + n) % n else r
> {code}
> The trailing {{% n}} is redundant _only when the divisor {{n > 0}}_. Since 
> {{a % n}} has magnitude strictly less than |n|, for {{r < 0}} and {{n > 0}} 
> the shifted value {{r + n}} already lies in {{[0, n)}} and the second modulo 
> is a no-op. For {{n < 0}}, {{r + n}} can fall below {{n}} and still needs 
> reducing, so the {{% n}} must be kept there.
> h3. Change
> {{MathUtils.pmod}} backs the {{pmod}} SQL function and the per-row 
> partition-id expression of {{HashPartitioning}} ({{Pmod(Murmur3Hash(...), 
> numPartitions)}}), so it runs once per row on the map side of every 
> hash-partitioned shuffle -- always with a positive divisor 
> ({{numPartitions}}). Removing the redundant modulo in that case takes an 
> integer division off the hot path.
> The {{Float}}/{{Double}} overloads are left unchanged on purpose: 
> floating-point rounding can round {{r + n}} up to exactly {{n}}, and the 
> extra {{% n}} folds that back to {{0}}, so there it is a real correctness 
> guard rather than a no-op.
> h3. Benchmark
> JMH microbenchmark of the three forms (16,384 {{pmod(int, int)}} calls per 
> op, JDK 17, JMH 1.37, 2 forks x 5 iterations, {{AverageTime}}):
> ||divisor n||OLD (r+n)%n||drop-all (rejected)||this change (guarded)||guarded 
> vs OLD||
> |8 (n > 0)|81.05 us/op|25.26 us/op|25.28 us/op|approx 3.2x faster|
> |200 (n > 0)|88.90 us/op|25.28 us/op|25.31 us/op|approx 3.5x faster|
> |-200 (n < 0)|87.46 us/op|25.28 us/op (*)|87.10 us/op|unchanged (correct)|
> For positive divisors the guarded form matches the unconditional drop (the 
> {{if (n > 0)}} test is free) and is about 3.2-3.5x faster than the original. 
> For negative divisors it correctly keeps the modulo, matching the original 
> cost and result. (*) the drop-all column at {{n < 0}} is fast only because it 
> skips the required modulo and returns the wrong, out-of-range value.
> h3. Correctness
> No user-facing change: the result is identical to released Spark for every 
> input, including negative divisors. {{pmod}} is only guaranteed non-negative 
> for a positive divisor; for a negative divisor it shares the sign of the 
> dividend ({{pmod(-3, -5) == -3}}), and that released behavior is preserved.
> Verified by cross-checking the guarded form for exact equality with the 
> original against an exhaustive oracle: all {{byte}} and {{short}} {{(a, n)}} 
> pairs, and all 2^32 {{int}} dividends across positive and negative divisors, 
> with zero mismatches. New regression cases in {{ArithmeticExpressionSuite}} 
> cover the {{r < 0, n < 0}} path (e.g. {{pmod(-3, -5) == -3}}) that an 
> unconditional drop would break and that existing tests did not exercise.



--
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