David Mollitor created SPARK-59696:
--------------------------------------

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


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