diff --git a/dev/design/performance-delivery-selection.md b/dev/design/performance-delivery-selection.md index e6b4c3b42..4c6494998 100644 --- a/dev/design/performance-delivery-selection.md +++ b/dev/design/performance-delivery-selection.md @@ -156,3 +156,22 @@ Keep the method recognizer deferred: next method work should begin with profiling and a design for a cost shared across ordinary methods, such as argument-frame allocation, cached dispatch, scalar-result handling, or hash access. + +## Current independent candidate: native-integer modulus + +Candidate commits `3a4dabb54` and `0cc1c3e32`, rebased onto current master +`24f445be0`, bypass overload lookup and coercion only when both operands are +plain, non-wide `INTEGER` scalars. Tied, overloaded, string, floating-point, +and wide-integer operands retain the existing slow path. + +- System-Perl semantic tests and focused JVM/interpreter coverage were present + on the candidate; the full immutable gate passed at + `/tmp/make-native-integer-modulus-current-20260918.log`. +- Seven-pair master/candidate production comparisons had matching checksums + and stabilized warmups: Numeric median +1.5% (`/tmp/perf-native-integer-modulus-numeric-7pair-20260918.json`), + Life median +2.9% (`/tmp/perf-native-integer-modulus-life-7pair-20260918.json`), + and unrelated String median +1.3% + (`/tmp/perf-native-integer-modulus-string-7pair-20260918.json`). +- Decision: retain for focused delivery review. These are direct + master/candidate comparisons under intended production load, not a claim of + portfolio parity. diff --git a/docs/about/changelog.md b/docs/about/changelog.md index 2a6e9ee11..74cf94fd6 100644 --- a/docs/about/changelog.md +++ b/docs/about/changelog.md @@ -6,6 +6,9 @@ priorities and future plans. ## Work in progress +- Fast-path ordinary native-integer modulus while preserving tied, overloaded, + string, floating-point, and wide-integer behavior. + - Restore `local` compatibility for tied hash and array elements, sparse arrays, magic stashes, implicit `$_` foreach aliases (including early return), and localized regex captures on both execution backends. diff --git a/src/main/java/org/perlonjava/runtime/operators/MathOperators.java b/src/main/java/org/perlonjava/runtime/operators/MathOperators.java index d9bcbf7af..1bbe5586f 100644 --- a/src/main/java/org/perlonjava/runtime/operators/MathOperators.java +++ b/src/main/java/org/perlonjava/runtime/operators/MathOperators.java @@ -835,6 +835,15 @@ public static RuntimeScalar modulus(RuntimeScalar arg1, RuntimeScalar arg2) { } private static RuntimeScalar modulusUnpropagated(RuntimeScalar arg1, RuntimeScalar arg2) { + // The overwhelmingly common numeric case needs neither overload + // lookup nor numeric coercion. Keep this before blessedId(): a + // blessed scalar cannot have the plain INTEGER representation. + if (arg1.type == INTEGER && arg2.type == INTEGER && !hasWideInteger(arg1, arg2)) { + return modulusFromLongs(arg1.getLong(), arg2.getLong()); + } + + // Preserve upstream's one-FETCH semantics before the general + // overload and coercion path. arg1 = RuntimeScalar.fetchTiedOnce(arg1); arg2 = RuntimeScalar.fetchTiedOnce(arg2); // Prepare overload context and check if object is eligible for overloading @@ -852,22 +861,7 @@ private static RuntimeScalar modulusUnpropagated(RuntimeScalar arg1, RuntimeScal return modulusFromDoubles(arg1.getDouble(), arg2.getDouble()); } - // Use long arithmetic to handle large integers (beyond int range) - long dividend = arg1.getLong(); - long divisor = arg2.getLong(); - long result = dividend % divisor; - - // Adjust result for Perl-style modulus behavior - // In Perl, the result has the same sign as the divisor - if (result != 0 && ((divisor > 0 && result < 0) || (divisor < 0 && result > 0))) { - result += divisor; - } - - // Return as int if it fits, otherwise as long - if (result >= Integer.MIN_VALUE && result <= Integer.MAX_VALUE) { - return new RuntimeScalar((int) result); - } - return new RuntimeScalar(result); + return modulusFromLongs(arg1.getLong(), arg2.getLong()); } /** @@ -883,6 +877,15 @@ public static RuntimeScalar modulusWarn(RuntimeScalar arg1, RuntimeScalar arg2) } private static RuntimeScalar modulusWarnUnpropagated(RuntimeScalar arg1, RuntimeScalar arg2) { + // Defined integer operands cannot emit an uninitialized warning, so + // they share the ordinary fast path while retaining outer taint + // propagation in modulusWarn(). + if (arg1.type == INTEGER && arg2.type == INTEGER && !hasWideInteger(arg1, arg2)) { + return modulusFromLongs(arg1.getLong(), arg2.getLong()); + } + + // Preserve upstream's one-FETCH semantics before the general + // overload and coercion path. arg1 = RuntimeScalar.fetchTiedOnce(arg1); arg2 = RuntimeScalar.fetchTiedOnce(arg2); // Prepare overload context and check if object is eligible for overloading @@ -901,22 +904,7 @@ private static RuntimeScalar modulusWarnUnpropagated(RuntimeScalar arg1, Runtime return modulusFromDoubles(arg1.getDouble(), arg2.getDouble()); } - // Use long arithmetic to handle large integers (beyond int range) - long dividend = arg1.getLong(); - long divisor = arg2.getLong(); - long result = dividend % divisor; - - // Adjust result for Perl-style modulus behavior - // In Perl, the result has the same sign as the divisor - if (result != 0 && ((divisor > 0 && result < 0) || (divisor < 0 && result > 0))) { - result += divisor; - } - - // Return as int if it fits, otherwise as long - if (result >= Integer.MIN_VALUE && result <= Integer.MAX_VALUE) { - return new RuntimeScalar((int) result); - } - return new RuntimeScalar(result); + return modulusFromLongs(arg1.getLong(), arg2.getLong()); } /** @@ -1260,7 +1248,7 @@ public static RuntimeScalar integerModulus(RuntimeScalar arg1, RuntimeScalar arg return new RuntimeScalar(result); } - /** Integer modulus with Perl's divisor-sign result rule. */ + /** Native-integer modulus with Perl's divisor-sign result rule. */ private static RuntimeScalar modulusFromLongs(long dividend, long divisor) { long result = dividend % divisor; if (result != 0 && ((divisor > 0 && result < 0) || (divisor < 0 && result > 0))) { diff --git a/src/test/resources/unit/math_modulus_integer_fast_path.t b/src/test/resources/unit/math_modulus_integer_fast_path.t new file mode 100644 index 000000000..866d35405 --- /dev/null +++ b/src/test/resources/unit/math_modulus_integer_fast_path.t @@ -0,0 +1,27 @@ +use strict; +use warnings; +use Test::More tests => 8; + +# INTEGER/INTEGER modulus is a hot arithmetic path. These cases cover the +# result-sign rule and the values which must remain on the native-integer path. +is(7 % 3, 1, 'positive dividend and divisor'); +is(-7 % 3, 2, 'positive divisor determines a negative dividend result sign'); +is(7 % -3, -2, 'negative divisor determines a positive dividend result sign'); +is(-7 % -3, -1, 'both negative operands preserve divisor sign'); + +my $large = 4_611_686_018_427_387_911; +is($large % 1_000_003, 837_681, 'large integer modulus remains exact'); + +my ($lexical, $global) = (11, 7); +for (1 .. 2_048) { + $lexical = ($lexical * 33 + $_) % 1_000_003; + $global = ($global + $lexical) % 1_000_003; +} +is($lexical ^ $global, 37_478, 'numeric workload recurrence remains stable'); + +my @warnings; +{ + local $SIG{__WARN__} = sub { push @warnings, @_ }; + is(17 % 5, 2, 'ordinary integer modulus has the expected result with warnings enabled'); +} +is_deeply(\@warnings, [], 'defined integer operands do not warn'); diff --git a/src/test/resources/unit/math_modulus_integer_fast_path_magic.t b/src/test/resources/unit/math_modulus_integer_fast_path_magic.t new file mode 100644 index 000000000..e024317d6 --- /dev/null +++ b/src/test/resources/unit/math_modulus_integer_fast_path_magic.t @@ -0,0 +1,29 @@ +use strict; +use warnings; +use Test::More tests => 5; + +{ + package Local::ModulusTie; + sub TIESCALAR { + my ($class, $value) = @_; + bless { value => $value, fetches => 0 }, $class; + } + sub FETCH { ++$_[0]{fetches}; $_[0]{value} } + sub STORE { $_[0]{value} = $_[1] } +} + +tie my $left, 'Local::ModulusTie', 17; +tie my $right, 'Local::ModulusTie', 5; +is($left % $right, 2, 'tied integer values use their FETCH results'); +is(tied($left)->{fetches}, 1, 'left tied operand is fetched exactly once'); +is(tied($right)->{fetches}, 1, 'right tied operand is fetched exactly once'); + +{ + package Local::ModulusOverload; + our $calls = 0; + use overload '%' => sub { ++$calls; 23 }, fallback => 1; +} + +my $overloaded = bless \(my $value = 17), 'Local::ModulusOverload'; +is($overloaded % 5, 23, 'overloaded modulus bypasses the native integer fast path'); +is($Local::ModulusOverload::calls, 1, 'overloaded modulus is dispatched once');