Skip to content

Fast path for {U}Int128.operator % when the modulus is within UInt64 #106915

Description

@hez2010

Description

The modulus operator for {U}Int128 is too slow:

public static UInt64 ModMul_1(UInt64 first, UInt64 second, UInt64 modulus)
{
    return unchecked((UInt64)((Math.BigMul(first, second) % modulus));
}

public static UInt64 ModMul_2(UInt64 first, UInt64 second, UInt64 modulus)
{
    return unchecked((UInt64)(((UInt128)first * second) % modulus));
}

Instead we can provide a fast path by using DivRem for cases where modulus falls in the range of UInt64. For example:

public static UInt128 operator %(UInt128 left, UInt128 right)
{
    if (X86Base.X64.IsSupported)
    {
        if (right._upper == 0)
        {
            return X86Base.X64.DivRem(left._lower, left._upper, right._lower).Remainder;
        }
    }
        
    return left % right;
}

Configuration

.NET 9 Preview 7

Regression?

No

Data

public class ModMulModule
{
    [Benchmark]
    [Arguments(0x123456789ABCDEF0, 0x123456789ABCDEF0, 0x123456789ABCDEF0)]
    public UInt64 ModMul_Slow(UInt64 first, UInt64 second, UInt64 modulus)
    {
        return unchecked((UInt64)(((UInt128)first * second) % modulus));
    }

    [Benchmark]
    [Arguments(0x123456789ABCDEF0, 0x123456789ABCDEF0, 0x123456789ABCDEF0)]
    public UInt64 ModMul_Fast(UInt64 first, UInt64 second, UInt64 modulus)
    {
        return unchecked((UInt64)Mod((UInt128)first * second, modulus));
    }

    public static UInt128 Mod(UInt128 left, UInt128 right)
    {
        if (GetUpper(in right) == 0)
        {
            if (X86Base.X64.IsSupported)
            {
                return X86Base.X64.DivRem(GetLower(in left), GetUpper(in left), GetLower(in right)).Remainder;
            }
        }

        return left % right;
    }

    [UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_upper")]
    static extern ref UInt64 GetUpper(ref readonly UInt128 value);

    [UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_lower")]
    static extern ref UInt64 GetLower(ref readonly UInt128 value);
}

BenchmarkDotNet v0.14.0, Windows 11 (10.0.26100.1457)
13th Gen Intel Core i7-13700K, 1 CPU, 24 logical and 16 physical cores
.NET SDK 9.0.100-preview.7.24407.12
[Host] : .NET 9.0.0 (9.0.24.40507), X64 RyuJIT AVX2
DefaultJob : .NET 9.0.0 (9.0.24.40507), X64 RyuJIT AVX2

Method Mean Error StdDev
ModMul_Slow 15.2984 ns 0.0508 ns 0.0475 ns
ModMul_Fast 0.4423 ns 0.0045 ns 0.0042 ns

Activity

  1. ghost added
    needs-area-labelAn area label is needed to ensure this gets routed to the appropriate area owners
    on Aug 24, 2024
  2. tannergooding commented on Aug 24, 2024

    @tannergooding
    Member

    The issue here isn't really that there is no "fast path", it's that it's implemented as left - (left / right) * right when instead it should be computing the remainder as part of the division, where its essentially "free".

    The cost is significantly higher with the multi-step process because we have to fallback to a full division and multiplication algorithm.

  3. added and removed
    needs-area-labelAn area label is needed to ensure this gets routed to the appropriate area owners
    on Aug 24, 2024
  4. added this to the 10.0.0 milestone on Aug 24, 2024
  5. removed
    untriagedNew issue has not been triaged by the area owner
    on Aug 24, 2024
  6. dotnet-policy-service commented on Aug 24, 2024

    @dotnet-policy-service
    Contributor

    Tagging subscribers to this area: @dotnet/area-system-numerics
    See info in area-owners.md if you want to be subscribed.

  7. modified the milestones: 10.0.0, Future on Jul 14, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions