Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions dev/design/performance-delivery-selection.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
3 changes: 3 additions & 0 deletions docs/about/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
54 changes: 21 additions & 33 deletions src/main/java/org/perlonjava/runtime/operators/MathOperators.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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());
}

/**
Expand All @@ -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
Expand All @@ -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());
}

/**
Expand Down Expand Up @@ -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))) {
Expand Down
27 changes: 27 additions & 0 deletions src/test/resources/unit/math_modulus_integer_fast_path.t
Original file line number Diff line number Diff line change
@@ -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');
29 changes: 29 additions & 0 deletions src/test/resources/unit/math_modulus_integer_fast_path_magic.t
Original file line number Diff line number Diff line change
@@ -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');
Loading