-
Notifications
You must be signed in to change notification settings - Fork 4.7k
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
Conversation
Tagging subscribers to this area: @dotnet/area-system-numerics Issue DetailsThe current implementation is a loop through the The benchmarks are an attempt to represent the worst case for
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();
}
|
Unfortunately |
478fe8b
to
9f7d047
Compare
There was a problem hiding this 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?
Thanks @adamsitnik for your efforts on the community PRs
Sure |
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 ofCompareTo
).