Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use SequenceEqual in BigInteger.Equals #91416

Merged
merged 1 commit into from
Oct 30, 2023
Merged

Conversation

Rob-Hague
Copy link
Contributor

@Rob-Hague Rob-Hague commented Aug 31, 2023

The current implementation is a loop through the _bits array from end to beginning (most significant bytes to least).

The benchmarks are an attempt to represent the worst case for SequenceEqual in the comparison: lengths with a remainder for vectorization; difference at the most significant byte. I am not aware of a reason to search MSB->LSB, other than the convenience of using a helper which does so (for the purpose of CompareTo).


BenchmarkDotNet v0.13.7, Windows 11 (10.0.22621.2283/22H2/2022Update/SunValley2)
AMD Ryzen 7 5700U with Radeon Graphics, 1 CPU, 16 logical and 8 physical cores
.NET SDK 8.0.100-rc.1.23455.8
  [Host]     : .NET 7.0.11 (7.0.1123.42427), X64 RyuJIT AVX2
  pr : .NET 9.0.0 (42.42.42.42424), X64 RyuJIT AVX2
  main : .NET 9.0.0 (42.42.42.42424), X64 RyuJIT AVX2


Method Job args Mean Error StdDev Ratio RatioSD
Equal pr 5 bytes, DiffMSB 4.048 ns 0.1106 ns 0.1787 ns 1.79 0.07
Equal main 5 bytes, DiffMSB 2.258 ns 0.0074 ns 0.0058 ns 1.00 0.00
Equal pr 5 bytes, Same 4.054 ns 0.1100 ns 0.1897 ns 1.39 0.07
Equal main 5 bytes, Same 2.951 ns 0.0027 ns 0.0024 ns 1.00 0.00
Equal pr 19 bytes, DiffMSB 4.257 ns 0.1144 ns 0.1848 ns 1.86 0.09
Equal main 19 bytes, DiffMSB 2.303 ns 0.0035 ns 0.0027 ns 1.00 0.00
Equal pr 19 bytes, Same 4.179 ns 0.1120 ns 0.1931 ns 0.91 0.04
Equal main 19 bytes, Same 4.738 ns 0.0066 ns 0.0062 ns 1.00 0.00
Equal pr 67 bytes, DiffMSB 4.536 ns 0.1185 ns 0.1108 ns 1.82 0.05
Equal main 67 bytes, DiffMSB 2.486 ns 0.0018 ns 0.0016 ns 1.00 0.00
Equal pr 67 bytes, Same 4.296 ns 0.0229 ns 0.0179 ns 0.36 0.00
Equal main 67 bytes, Same 11.991 ns 0.0173 ns 0.0162 ns 1.00 0.00
Equal pr 259 bytes, DiffMSB 9.818 ns 0.2209 ns 0.2268 ns 4.32 0.11
Equal main 259 bytes, DiffMSB 2.278 ns 0.0328 ns 0.0291 ns 1.00 0.00
Equal pr 259 bytes, Same 9.697 ns 0.0599 ns 0.0561 ns 0.28 0.00
Equal main 259 bytes, Same 35.024 ns 0.0513 ns 0.0454 ns 1.00 0.00
public class Benchmarks
{
    public IEnumerable<object> GetArguments()
    {            
        Random rnd = new Random(123456);

        foreach (int byteCount in new[] { 5, 19, 67, 259 })
        {
            byte[] bytes = new byte[byteCount];

            do
            {
                rnd.NextBytes(bytes);
            } while (bytes[^1] is not (> 0 and < 128));

            BigInteger b1 = new(bytes);

            yield return new Arguments(b1, new BigInteger(bytes), $"{byteCount} bytes, Same");

            bytes[^1] = (byte)(~bytes[^1] & 0x7F);

            yield return new Arguments(b1, new BigInteger(bytes), $"{byteCount} bytes, DiffMSB");
        }
    }

    public class Arguments
    {
        private readonly BigInteger _b1;
        private readonly BigInteger _b2;
        private readonly string _description;

        public Arguments(BigInteger b1, BigInteger b2, string description)
        {
            _b1 = b1;
            _b2 = b2;
            _description = description;
        }

        public bool AreEqual() => _b1 == _b2;

        public override string ToString() => _description;
    }

    [Benchmark]
    [ArgumentsSource(nameof(GetArguments))]
    public bool Equal(Arguments args) => args.AreEqual();
}

@ghost ghost added the community-contribution Indicates that the PR has been added by a community member label Aug 31, 2023
@ghost
Copy link

ghost commented Aug 31, 2023

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

Issue Details

The current implementation is a loop through the _bits array from end to beginning (most significant bytes to least).

The benchmarks are an attempt to represent the worst case for SequenceEqual in the comparison: lengths with a remainder for vectorization; difference at the most significant byte. I am not aware of a reason to search MSB->LSB, other than the convenience of using a helper which does so (for the purpose of CompareTo).


BenchmarkDotNet v0.13.7, Windows 11 (10.0.22621.2134/22H2/2022Update/SunValley2)
AMD Ryzen 7 5700U with Radeon Graphics, 1 CPU, 16 logical and 8 physical cores
.NET SDK 8.0.100-preview.7.23376.3
  [Host]     : .NET 7.0.4 (7.0.423.11508), X64 RyuJIT AVX2
  pr : .NET 9.0.0 (42.42.42.42424), X64 RyuJIT AVX2
  main : .NET 9.0.0 (42.42.42.42424), X64 RyuJIT AVX2


Method Job args Mean Error StdDev Ratio RatioSD
Equal pr 5 bytes, DiffMSB 3.329 ns 0.0944 ns 0.1551 ns 1.54 0.09
Equal main 5 bytes, DiffMSB 2.181 ns 0.0554 ns 0.0518 ns 1.00 0.00
Equal pr 5 bytes, Same 3.324 ns 0.0948 ns 0.1164 ns 1.20 0.04
Equal main 5 bytes, Same 2.750 ns 0.0028 ns 0.0025 ns 1.00 0.00
Equal pr 19 bytes, DiffMSB 3.351 ns 0.0975 ns 0.1268 ns 1.59 0.07
Equal main 19 bytes, DiffMSB 2.126 ns 0.0376 ns 0.0314 ns 1.00 0.00
Equal pr 19 bytes, Same 3.811 ns 0.0282 ns 0.0264 ns 0.81 0.01
Equal main 19 bytes, Same 4.728 ns 0.0037 ns 0.0035 ns 1.00 0.00
Equal pr 67 bytes, DiffMSB 3.739 ns 0.0879 ns 0.0780 ns 1.81 0.03
Equal main 67 bytes, DiffMSB 2.054 ns 0.0015 ns 0.0011 ns 1.00 0.00
Equal pr 67 bytes, Same 3.972 ns 0.0864 ns 0.0808 ns 0.34 0.01
Equal main 67 bytes, Same 11.737 ns 0.0213 ns 0.0188 ns 1.00 0.00
Equal pr 259 bytes, DiffMSB 6.760 ns 0.1618 ns 0.2917 ns 3.30 0.12
Equal main 259 bytes, DiffMSB 2.053 ns 0.0021 ns 0.0016 ns 1.00 0.00
Equal pr 259 bytes, Same 6.928 ns 0.1388 ns 0.1298 ns 0.20 0.00
Equal main 259 bytes, Same 34.792 ns 0.0447 ns 0.0397 ns 1.00 0.00
public class Benchmarks
{
    public IEnumerable<object> GetArguments()
    {            
        Random rnd = new Random(123456);

        foreach (int byteCount in new[] { 5, 19, 67, 259 })
        {
            byte[] bytes = new byte[byteCount];

            do
            {
                rnd.NextBytes(bytes);
            } while (bytes[^1] is not (> 0 and < 128));

            BigInteger b1 = new(bytes);

            yield return new Arguments(b1, new BigInteger(bytes), $"{byteCount} bytes, Same");

            bytes[^1] = (byte)(~bytes[^1] & 0x7F);

            yield return new Arguments(b1, new BigInteger(bytes), $"{byteCount} bytes, DiffMSB");
        }
    }

    public class Arguments
    {
        private readonly BigInteger _b1;
        private readonly BigInteger _b2;
        private readonly string _description;

        public Arguments(BigInteger b1, BigInteger b2, string description)
        {
            _b1 = b1;
            _b2 = b2;
            _description = description;
        }

        public bool AreEqual() => _b1 == _b2;

        public override string ToString() => _description;
    }

    [Benchmark]
    [ArgumentsSource(nameof(GetArguments))]
    public bool Equal(Arguments args) => args.AreEqual();
}
Author: Rob-Hague
Assignees: -
Labels:

area-System.Numerics, community-contribution

Milestone: -

@huoyaoyuan
Copy link
Member

Unfortunately SequenceCompareTo is in the reversed order and can't be used in CompareTo.

@adamsitnik adamsitnik self-assigned this Oct 30, 2023
@adamsitnik adamsitnik added the tenet-performance Performance related issue label Oct 30, 2023
@adamsitnik adamsitnik added this to the 9.0.0 milestone Oct 30, 2023
Copy link
Member

@adamsitnik adamsitnik left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've extended your benchmark to focus not only on the worst case, but also best and middle.

public IEnumerable<object> GetArguments()
{
    Random rnd = new Random(123456);

    foreach (int byteCount in new[] { 5, 19, 67, 259 })
    {
        byte[] bytes = new byte[byteCount];

        do
        {
            rnd.NextBytes(bytes);
        } while (bytes[^1] is not (> 0 and < 128));

        BigInteger b1 = new(bytes);
        yield return new Arguments(b1, new BigInteger(bytes), $"{byteCount} bytes, Same");

        byte copy = bytes[^1];
        bytes[^1] = (byte)(~bytes[^1] & 0x7F);
        yield return new Arguments(b1, new BigInteger(bytes), $"{byteCount} bytes, DiffLastByte");
        bytes[^1] = copy;

        copy = bytes[0];
        bytes[0] = (byte)(~bytes[0] & 0x7F);
        yield return new Arguments(b1, new BigInteger(bytes), $"{byteCount} bytes, DiffFirstByte");
        bytes[0] = copy;

        copy = bytes[byteCount / 2];
        bytes[byteCount / 2] = (byte)(~bytes[byteCount / 2] & 0x7F);
        yield return new Arguments(b1, new BigInteger(bytes), $"{byteCount} bytes, DiffMiddleByte");
        bytes[byteCount / 2] = copy;
    }
}

public class Arguments
{
    private readonly BigInteger _b1;
    private readonly BigInteger _b2;
    private readonly string _description;

    public Arguments(BigInteger b1, BigInteger b2, string description)
    {
        _b1 = b1;
        _b2 = b2;
        _description = description;
    }

    public bool AreEqual() => _b1 == _b2;

    public override string ToString() => _description;
}

[Benchmark]
[ArgumentsSource(nameof(GetArguments))]
public bool Equal(Arguments args) => args.AreEqual();
BenchmarkDotNet v0.13.7-nightly.20230717.35, Windows 11 (10.0.22621.2428/22H2/2022Update/SunValley2)
AMD Ryzen Threadripper PRO 3945WX 12-Cores, 1 CPU, 24 logical and 12 physical cores
.NET SDK 8.0.100-rc.2.23429.6
  [Host]     : .NET 8.0.0 (8.0.23.42604), X64 RyuJIT AVX2
        main : .NET 9.0.0 (42.42.42.42424), X64 RyuJIT AVX2
          PR : .NET 9.0.0 (42.42.42.42424), X64 RyuJIT AVX2
Method Job args Mean Ratio
Equal main 19 bytes, DiffFirstByte 5.228 ns 1.00
Equal PR 19 bytes, DiffFirstByte 2.423 ns 0.46
Equal main 19 bytes, DiffLastByte 2.375 ns 1.00
Equal PR 19 bytes, DiffLastByte 2.832 ns 1.19
Equal main 19 bytes, DiffMiddleByte 3.733 ns 1.00
Equal PR 19 bytes, DiffMiddleByte 2.310 ns 0.62
Equal main 19 bytes, Same 4.877 ns 1.00
Equal PR 19 bytes, Same 2.795 ns 0.57
Equal main 259 bytes, DiffFirstByte 42.573 ns 1.00
Equal PR 259 bytes, DiffFirstByte 2.157 ns 0.05
Equal main 259 bytes, DiffLastByte 2.368 ns 1.00
Equal PR 259 bytes, DiffLastByte 4.967 ns 2.10
Equal main 259 bytes, DiffMiddleByte 21.282 ns 1.00
Equal PR 259 bytes, DiffMiddleByte 3.948 ns 0.19
Equal main 259 bytes, Same 38.730 ns 1.00
Equal PR 259 bytes, Same 4.951 ns 0.13
Equal main 5 bytes, DiffFirstByte 3.066 ns 1.00
Equal PR 5 bytes, DiffFirstByte 2.575 ns 0.84
Equal main 5 bytes, DiffLastByte 2.339 ns 1.00
Equal PR 5 bytes, DiffLastByte 2.565 ns 1.10
Equal main 5 bytes, DiffMiddleByte 3.059 ns 1.00
Equal PR 5 bytes, DiffMiddleByte 2.556 ns 0.84
Equal main 5 bytes, Same 3.028 ns 1.00
Equal PR 5 bytes, Same 2.541 ns 0.84
Equal main 67 bytes, DiffFirstByte 13.271 ns 1.00
Equal PR 67 bytes, DiffFirstByte 2.142 ns 0.16
Equal main 67 bytes, DiffLastByte 2.324 ns 1.00
Equal PR 67 bytes, DiffLastByte 3.036 ns 1.31
Equal main 67 bytes, DiffMiddleByte 9.541 ns 1.00
Equal PR 67 bytes, DiffMiddleByte 2.814 ns 0.29
Equal main 67 bytes, Same 12.028 ns 1.00
Equal PR 67 bytes, Same 3.272 ns 0.27

As long as comparing BigIntegers that are different in the last few bytes is not super common, it's a good trade off to made 👍

@Rob-Hague big thanks for your contribution!

Would you mind adding the benchmarks to https://github.com/dotnet/performance/blob/main/src/benchmarks/micro/libraries/System.Runtime.Numerics/Perf.BigInteger.cs?

@adamsitnik adamsitnik merged commit 44a5abd into dotnet:main Oct 30, 2023
109 checks passed
@Rob-Hague Rob-Hague deleted the bigintequals branch October 30, 2023 15:55
@Rob-Hague
Copy link
Contributor Author

Thanks @adamsitnik for your efforts on the community PRs

Would you mind adding the benchmarks to

Sure

@ghost ghost locked as resolved and limited conversation to collaborators Nov 29, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area-System.Numerics community-contribution Indicates that the PR has been added by a community member tenet-performance Performance related issue
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants