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

Optimized the CompareTo and Equals methods (FractionValueEqualityComparer) #71

Merged
merged 1 commit into from
Jul 1, 2024

Conversation

lipchev
Copy link
Contributor

@lipchev lipchev commented Jun 10, 2024

  • Optimized the CompareTo and Equals methods (FractionValueEqualityComparer)
  • Updated the benchmark results and the Readme.md (for the benchmarks)

…arer).

Updated the benchmark results and the Readme.md (for the benchmarks)
@lipchev
Copy link
Contributor Author

lipchev commented Jun 10, 2024

Here are the absolute differences (in nanoseconds) for the NormalizedFractionComparisonBenchmarks (v7 is the master branch , v8 is this branch):

image

And here how we stack-up against the StrictEqualityEquals:

image

@lipchev
Copy link
Contributor Author

lipchev commented Jun 10, 2024

And here are the results of the NonNormalizedComparisonBenchmarks (absolute differences in nanoseconds):

image

And here are the absolute results- here I'm going to give the GetHashCode method to compare against (with the extra GCD-reduction):

image

Now, typically this isn't suppose to be like that- and for most scenarios it would probably be faster to just return 0 (or maybe something like HashCode.Combine(Numerator/Denominator, Denominator/Numerator)) - but then again, it depends on how many other properties are part of the larger object's hash-code- so I'm not sure whether to recommend it or not..

@danm-de
Copy link
Owner

danm-de commented Jun 16, 2024

Thank you for taking the time to optimize the two methods. I don't have much free time, so I've only planned small time windows for code reviews. Unfortunately, even after the second attempt, I was unable to understand the algorithms implemented in the pull request.

Admittedly, that could also be due to my advancing age or my lack of love for mathematics in general ;-)

Did you get this from a book - something where I can find out the mathematical correctness? I don't want to rely on unit tests alone here. I still find the current code easy to understand (also taking into account the special treatment for NaN/Infinity). I'm not sure the performance gains (which are in the nanosecond range) are worth it.

@lipchev
Copy link
Contributor Author

lipchev commented Jun 16, 2024

I need to go out in 5 minutes so I'll try to make it quick (will elaborate later if necessary):
I don't have a particular reference but here's the reasoning behind the Equals:
Let's consider an example: 5 / 3 = 1(q) + 2 (r) / 3 and 10 / 6 = 1 (q) + 4 (r) / 6

  1. If {n1/d1} == {n2/d2} then the integer division (a.k.a. the quotient) between {n1/d1} should be the same as the the one between {n2/d2}
  2. The same should be true if we inverted the fractions (which we do as Smaller / Larger is always 0)
  3. If the quotients are the same we examine the remainders of the division- their ratio, compared to the dividend should be the same: e.g 5 / 3 = 1 + 2 / 3 , same as 10 / 6 = 1 + 4 / 6 -> 2/3 and 4/6 should be the same.

Comment on lines +86 to +89
var numeratorQuotient = BigInteger.DivRem(numerator1, numerator2, out var remainderNumerators);
var denominatorQuotient = BigInteger.DivRem(denominator1, denominator2, out var remainderDenominators);
// if the fractions are equal: numeratorQuotient should be equal to denominatorQuotient
if (numeratorQuotient != denominatorQuotient) {
Copy link
Contributor Author

@lipchev lipchev Jun 16, 2024

Choose a reason for hiding this comment

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

This is the most interesting part:
What we're doing is taking a roundabout way of comparing the fractions by checking if their ratio is exactly 1 :
A / B == 1 -> A == B
Expressing this using the 4 terms gives us {n1, d1} / {n2, d2} = {(n1 * d2) / (d1 * n2)} = {n1 / n2} * {d2 / d1}
When we set {n1 / n2} * {d2 / d1} = 1 (and having previously ensured that the terms are non-negative)
It follows that {d1 / d2} = {n1 / n2} should be true (in order for A to be equal to B)

From here on we do the standard DivRem comparison (split into separate operations here, for simplicity):
{d1 / d2} = {n1 / n2} =>

  1. BigInteger.Divide(d1, d2) == BigInteger.Divide(n1, n2) // the quotients should be equal
  2. (d1 % d2) / d2 == (n1 % n2) / n2 // the remainders (r1, r2) expressed as a ratio (Fraction) of their respective dividend should be equal

If all conditions along the way are satisfied, we still end up comparing the product of 2 terms, but for most other cases we exit early.
Furthermore, the reason for selecting this particular set of operations is not accidental (it would have been simpler to just compare {n1/d1} and {n2/d2}): the reason is that if the two fractions are actually equal, then the result of dividing the two fractions would be something like {10/10} or {200/200} (but never {1/1} since d1>d2). In any way, this value is almost certainly not huge and the final 2 products (remainderNumerators * denominator2 and remainderDenominators * numerator2) would have values that are smaller that the original cross-term multiplication (due to remainderDenominators < denominator2 < denominator1).

Copy link
Owner

Choose a reason for hiding this comment

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

{(n1 * d2) / (d1 * n2)} = {n1 / n2} * {d2 / d1}

Ah, after reproducing the calculation on paper (or unforming the formula accordingly), I see that this equation is correct. Thanks.

Copy link
Owner

Choose a reason for hiding this comment

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

FYI - the code reviews take longer because I can't always understand your thought process based on the code (especially when mathematical equations need to be transformed).

}

// comparing the positive term ratios:
// {9/7} / {4/3} = {(2 + 1/4) / (2 + 1/3)} = {(9/4)/(7/3)} = {27/28} => (2).CompareTo(2) == 0 and (1/4).CompareTo(1/3) == -1
Copy link
Owner

@danm-de danm-de Jun 23, 2024

Choose a reason for hiding this comment

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

Can you please explain how this comment should be understood?

That's what I would see here:

{9/7} / {4/3} = (1 + 2/7) / (1 + 1/3) => (1).CompareTo(1) == 0 AND (2/7).CompareTo(1/3) == -1

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm doing the same roundabout comparison again- dividing the two fractions (like with the Equals) and then checking if their ratio is larger or smaller than {1/1}.
Another way of thinking about these divisions is to imagine that the terms represent decimals: {9.0 / 7.0} / {4.0 / 3.0} = {(9.0/4.0) / (7.0/3.0) = {2.25 / 2.33} . The part preceding the decimal separator (2) is the quotient, while the rest is the remainder divided by the dividend (e.g. 1/4 adjusted to denominator of 100 is 25/100).

@danm-de
Copy link
Owner

danm-de commented Jun 26, 2024

I'm done with the code review (algorithm understood). For testing, I added the previous CompareTo method (V1) and a variation for an A/B test.

See pull-request: https://github.com/danm-de/Fractions/pull/73/files#diff-b9514275b55edffc0f210fa857e59e0f1d706c067c44164fdd805e86fe77c498

The benchmark results on my computer (Intel Core i7-8650U) are... difficult to interpret. Actually, they are not clear to me - I cannot tell which algorithm is the "winner".

I also feel that the benchmark must not only focus on the edge cases (or "extreme values"). Which numbers are likely? And what magnitude are these numbers?

Fractions.Benchmarks.NonNormalizedComparisonBenchmarks-report.csv

Fractions.Benchmarks.NonNormalizedComparisonBenchmarks-report-github.md

@lipchev
Copy link
Contributor Author

lipchev commented Jun 27, 2024

The benchmark results on my computer (Intel Core i7-8650U) are... difficult to interpret. Actually, they are not clear to me - I cannot tell which algorithm is the "winner".

Yes, the *ComparisonBenchmarks have become a little overwhelming to run / read though.. I'll post a chart with your results in a bit..

While preparing the charts, I thought I'd run the benchmark with only these methods:

    [Benchmark(Baseline = true)]
    [ArgumentsSource(nameof(Operands))]
    public int CompareTo(Fraction a, Fraction b) {
        return a.CompareTo(b);
    }

    [Benchmark]
    [ArgumentsSource(nameof(Operands))]
    public int CompareToV1(Fraction a, Fraction b) {
        return a.CompareToV1(b);
    }

    [Benchmark]
    [ArgumentsSource(nameof(Operands))]
    public int CompareToV2(Fraction a, Fraction b) {
        return a.CompareToV2(b);
    }

Here are the results:


BenchmarkDotNet v0.13.12, Windows 10 (10.0.19045.4529/22H2/2022Update)
AMD Ryzen 9 7900X, 1 CPU, 24 logical and 12 physical cores
.NET SDK 8.0.302
  [Host]                      : .NET 8.0.6 (8.0.624.26715), X64 RyuJIT AVX-512F+CD+BW+DQ+VL+VBMI
  ShortRun-.NET 8.0           : .NET 8.0.6 (8.0.624.26715), X64 RyuJIT AVX-512F+CD+BW+DQ+VL+VBMI
  ShortRun-.NET Framework 4.8 : .NET Framework 4.8.1 (4.8.9241.0), X64 RyuJIT VectorSize=256

IterationCount=3  LaunchCount=1  WarmupCount=3
Method Job Runtime a b Mean Error StdDev Ratio RatioSD Gen0 Allocated Alloc Ratio
CompareTo ShortRun-.NET 8.0 .NET 8.0 -1000(...)000/1 [41] 1/1000000000000 7.847 ns 1.7736 ns 0.0972 ns 1.00 0.00 - - NA
CompareToV1 ShortRun-.NET 8.0 .NET 8.0 -1000(...)000/1 [41] 1/1000000000000 27.405 ns 0.8876 ns 0.0487 ns 3.49 0.04 0.0029 48 B NA
CompareToV2 ShortRun-.NET 8.0 .NET 8.0 -1000(...)000/1 [41] 1/1000000000000 27.016 ns 3.6881 ns 0.2022 ns 3.44 0.02 0.0029 48 B NA
CompareTo ShortRun-.NET Framework 4.8 .NET Framework 4.8 -1000(...)000/1 [41] 1/1000000000000 10.427 ns 3.8467 ns 0.2108 ns 1.00 0.00 - - NA
CompareToV1 ShortRun-.NET Framework 4.8 .NET Framework 4.8 -1000(...)000/1 [41] 1/1000000000000 77.045 ns 2.1793 ns 0.1195 ns 7.39 0.16 0.0076 48 B NA
CompareToV2 ShortRun-.NET Framework 4.8 .NET Framework 4.8 -1000(...)000/1 [41] 1/1000000000000 77.093 ns 3.3612 ns 0.1842 ns 7.40 0.16 0.0076 48 B NA
CompareTo ShortRun-.NET 8.0 .NET 8.0 -1024/1 -1/1024 12.216 ns 1.3993 ns 0.0767 ns 1.00 0.00 - - NA
CompareToV1 ShortRun-.NET 8.0 .NET 8.0 -1024/1 -1/1024 15.115 ns 0.2528 ns 0.0139 ns 1.24 0.01 - - NA
CompareToV2 ShortRun-.NET 8.0 .NET 8.0 -1024/1 -1/1024 15.204 ns 0.5059 ns 0.0277 ns 1.24 0.01 - - NA
CompareTo ShortRun-.NET Framework 4.8 .NET Framework 4.8 -1024/1 -1/1024 28.699 ns 1.1882 ns 0.0651 ns 1.00 0.00 - - NA
CompareToV1 ShortRun-.NET Framework 4.8 .NET Framework 4.8 -1024/1 -1/1024 39.675 ns 2.3270 ns 0.1275 ns 1.38 0.00 - - NA
CompareToV2 ShortRun-.NET Framework 4.8 .NET Framework 4.8 -1024/1 -1/1024 48.051 ns 3.4905 ns 0.1913 ns 1.67 0.01 - - NA
CompareTo ShortRun-.NET 8.0 .NET 8.0 -45/1 1/6 7.727 ns 0.2972 ns 0.0163 ns 1.00 0.00 - - NA
CompareToV1 ShortRun-.NET 8.0 .NET 8.0 -45/1 1/6 14.989 ns 0.4326 ns 0.0237 ns 1.94 0.01 - - NA
CompareToV2 ShortRun-.NET 8.0 .NET 8.0 -45/1 1/6 14.720 ns 0.3111 ns 0.0171 ns 1.90 0.01 - - NA
CompareTo ShortRun-.NET Framework 4.8 .NET Framework 4.8 -45/1 1/6 16.501 ns 0.3948 ns 0.0216 ns 1.00 0.00 - - NA
CompareToV1 ShortRun-.NET Framework 4.8 .NET Framework 4.8 -45/1 1/6 39.597 ns 4.0742 ns 0.2233 ns 2.40 0.02 - - NA
CompareToV2 ShortRun-.NET Framework 4.8 .NET Framework 4.8 -45/1 1/6 34.347 ns 2.8288 ns 0.1551 ns 2.08 0.01 - - NA
CompareTo ShortRun-.NET 8.0 .NET 8.0 42/-96 36/-96 4.615 ns 0.9048 ns 0.0496 ns 1.00 0.00 - - NA
CompareToV1 ShortRun-.NET 8.0 .NET 8.0 42/-96 36/-96 9.706 ns 0.8461 ns 0.0464 ns 2.10 0.02 - - NA
CompareToV2 ShortRun-.NET 8.0 .NET 8.0 42/-96 36/-96 8.656 ns 0.5922 ns 0.0325 ns 1.88 0.02 - - NA
CompareTo ShortRun-.NET Framework 4.8 .NET Framework 4.8 42/-96 36/-96 18.158 ns 0.6278 ns 0.0344 ns 1.00 0.00 - - NA
CompareToV1 ShortRun-.NET Framework 4.8 .NET Framework 4.8 42/-96 36/-96 12.901 ns 1.5062 ns 0.0826 ns 0.71 0.01 - - NA
CompareToV2 ShortRun-.NET Framework 4.8 .NET Framework 4.8 42/-96 36/-96 18.590 ns 0.8821 ns 0.0484 ns 1.02 0.00 - - NA
CompareTo ShortRun-.NET 8.0 .NET 8.0 0/1 1/1 8.257 ns 0.3669 ns 0.0201 ns 1.00 0.00 - - NA
CompareToV1 ShortRun-.NET 8.0 .NET 8.0 0/1 1/1 8.106 ns 0.5070 ns 0.0278 ns 0.98 0.00 - - NA
CompareToV2 ShortRun-.NET 8.0 .NET 8.0 0/1 1/1 7.889 ns 0.2347 ns 0.0129 ns 0.96 0.00 - - NA
CompareTo ShortRun-.NET Framework 4.8 .NET Framework 4.8 0/1 1/1 17.700 ns 1.5732 ns 0.0862 ns 1.00 0.00 - - NA
CompareToV1 ShortRun-.NET Framework 4.8 .NET Framework 4.8 0/1 1/1 16.290 ns 1.6533 ns 0.0906 ns 0.92 0.01 - - NA
CompareToV2 ShortRun-.NET Framework 4.8 .NET Framework 4.8 0/1 1/1 17.538 ns 1.6726 ns 0.0917 ns 0.99 0.00 - - NA
CompareTo ShortRun-.NET 8.0 .NET 8.0 77/3600 37/3600 8.226 ns 0.6972 ns 0.0382 ns 1.00 0.00 - - NA
CompareToV1 ShortRun-.NET 8.0 .NET 8.0 77/3600 37/3600 8.923 ns 0.9283 ns 0.0509 ns 1.08 0.00 - - NA
CompareToV2 ShortRun-.NET 8.0 .NET 8.0 77/3600 37/3600 8.140 ns 0.1280 ns 0.0070 ns 0.99 0.01 - - NA
CompareTo ShortRun-.NET Framework 4.8 .NET Framework 4.8 77/3600 37/3600 16.263 ns 0.2906 ns 0.0159 ns 1.00 0.00 - - NA
CompareToV1 ShortRun-.NET Framework 4.8 .NET Framework 4.8 77/3600 37/3600 18.076 ns 0.6012 ns 0.0330 ns 1.11 0.00 - - NA
CompareToV2 ShortRun-.NET Framework 4.8 .NET Framework 4.8 77/3600 37/3600 17.777 ns 2.2387 ns 0.1227 ns 1.09 0.01 - - NA
CompareTo ShortRun-.NET 8.0 .NET 8.0 135/1000 76/1000 8.335 ns 0.1127 ns 0.0062 ns 1.00 0.00 - - NA
CompareToV1 ShortRun-.NET 8.0 .NET 8.0 135/1000 76/1000 8.237 ns 0.2859 ns 0.0157 ns 0.99 0.00 - - NA
CompareToV2 ShortRun-.NET 8.0 .NET 8.0 135/1000 76/1000 7.896 ns 0.2065 ns 0.0113 ns 0.95 0.00 - - NA
CompareTo ShortRun-.NET Framework 4.8 .NET Framework 4.8 135/1000 76/1000 16.214 ns 1.6982 ns 0.0931 ns 1.00 0.00 - - NA
CompareToV1 ShortRun-.NET Framework 4.8 .NET Framework 4.8 135/1000 76/1000 17.841 ns 0.5056 ns 0.0277 ns 1.10 0.00 - - NA
CompareToV2 ShortRun-.NET Framework 4.8 .NET Framework 4.8 135/1000 76/1000 17.790 ns 0.1561 ns 0.0086 ns 1.10 0.01 - - NA
CompareTo ShortRun-.NET 8.0 .NET 8.0 27/200 19/250 11.594 ns 0.2187 ns 0.0120 ns 1.00 0.00 - - NA
CompareToV1 ShortRun-.NET 8.0 .NET 8.0 27/200 19/250 21.390 ns 0.1944 ns 0.0107 ns 1.84 0.00 - - NA
CompareToV2 ShortRun-.NET 8.0 .NET 8.0 27/200 19/250 20.849 ns 0.7985 ns 0.0438 ns 1.80 0.00 - - NA
CompareTo ShortRun-.NET Framework 4.8 .NET Framework 4.8 27/200 19/250 28.997 ns 0.9258 ns 0.0507 ns 1.00 0.00 - - NA
CompareToV1 ShortRun-.NET Framework 4.8 .NET Framework 4.8 27/200 19/250 56.556 ns 5.3424 ns 0.2928 ns 1.95 0.01 - - NA
CompareToV2 ShortRun-.NET Framework 4.8 .NET Framework 4.8 27/200 19/250 58.131 ns 17.3415 ns 0.9505 ns 2.00 0.03 - - NA
CompareTo ShortRun-.NET 8.0 .NET 8.0 42/66 36/96 11.779 ns 0.5400 ns 0.0296 ns 1.00 0.00 - - NA
CompareToV1 ShortRun-.NET 8.0 .NET 8.0 42/66 36/96 21.215 ns 0.8733 ns 0.0479 ns 1.80 0.01 - - NA
CompareToV2 ShortRun-.NET 8.0 .NET 8.0 42/66 36/96 21.718 ns 2.8140 ns 0.1542 ns 1.84 0.01 - - NA
CompareTo ShortRun-.NET Framework 4.8 .NET Framework 4.8 42/66 36/96 29.194 ns 2.8356 ns 0.1554 ns 1.00 0.00 - - NA
CompareToV1 ShortRun-.NET Framework 4.8 .NET Framework 4.8 42/66 36/96 55.284 ns 7.0987 ns 0.3891 ns 1.89 0.01 - - NA
CompareToV2 ShortRun-.NET Framework 4.8 .NET Framework 4.8 42/66 36/96 55.938 ns 4.5642 ns 0.2502 ns 1.92 0.02 - - NA
CompareTo ShortRun-.NET 8.0 .NET 8.0 70742(...)85248 [33] 70742(...)70496 [33] 15.854 ns 1.4853 ns 0.0814 ns 1.00 0.00 - - NA
CompareToV1 ShortRun-.NET 8.0 .NET 8.0 70742(...)85248 [33] 70742(...)70496 [33] 48.066 ns 18.2464 ns 1.0001 ns 3.03 0.08 0.0048 80 B NA
CompareToV2 ShortRun-.NET 8.0 .NET 8.0 70742(...)85248 [33] 70742(...)70496 [33] 47.432 ns 14.9475 ns 0.8193 ns 2.99 0.06 0.0048 80 B NA
CompareTo ShortRun-.NET Framework 4.8 .NET Framework 4.8 70742(...)85248 [33] 70742(...)70496 [33] 36.748 ns 4.9456 ns 0.2711 ns 1.00 0.00 - - NA
CompareToV1 ShortRun-.NET Framework 4.8 .NET Framework 4.8 70742(...)85248 [33] 70742(...)70496 [33] 125.590 ns 12.3811 ns 0.6786 ns 3.42 0.04 0.0126 80 B NA
CompareToV2 ShortRun-.NET Framework 4.8 .NET Framework 4.8 70742(...)85248 [33] 70742(...)70496 [33] 123.974 ns 52.0817 ns 2.8548 ns 3.37 0.09 0.0126 80 B NA
CompareTo ShortRun-.NET 8.0 .NET 8.0 245850922/78256779 NaN 8.262 ns 0.5993 ns 0.0329 ns 1.00 0.00 - - NA
CompareToV1 ShortRun-.NET 8.0 .NET 8.0 245850922/78256779 NaN 3.185 ns 1.6279 ns 0.0892 ns 0.39 0.01 - - NA
CompareToV2 ShortRun-.NET 8.0 .NET 8.0 245850922/78256779 NaN 7.754 ns 0.5741 ns 0.0315 ns 0.94 0.00 - - NA
CompareTo ShortRun-.NET Framework 4.8 .NET Framework 4.8 245850922/78256779 NaN 15.455 ns 0.9087 ns 0.0498 ns 1.00 0.00 - - NA
CompareToV1 ShortRun-.NET Framework 4.8 .NET Framework 4.8 245850922/78256779 NaN 15.426 ns 0.7980 ns 0.0437 ns 1.00 0.00 - - NA
CompareToV2 ShortRun-.NET Framework 4.8 .NET Framework 4.8 245850922/78256779 NaN 16.973 ns 0.5470 ns 0.0300 ns 1.10 0.00 - - NA
CompareTo ShortRun-.NET 8.0 .NET 8.0 245850922/78256779 -? 8.247 ns 0.5482 ns 0.0301 ns 1.00 0.00 - - NA
CompareToV1 ShortRun-.NET 8.0 .NET 8.0 245850922/78256779 -? 7.764 ns 0.2795 ns 0.0153 ns 0.94 0.00 - - NA
CompareToV2 ShortRun-.NET 8.0 .NET 8.0 245850922/78256779 -? 7.855 ns 2.6922 ns 0.1476 ns 0.95 0.02 - - NA
CompareTo ShortRun-.NET Framework 4.8 .NET Framework 4.8 245850922/78256779 -? 16.944 ns 0.1489 ns 0.0082 ns 1.00 0.00 - - NA
CompareToV1 ShortRun-.NET Framework 4.8 .NET Framework 4.8 245850922/78256779 -? 17.419 ns 0.3314 ns 0.0182 ns 1.03 0.00 - - NA
CompareToV2 ShortRun-.NET Framework 4.8 .NET Framework 4.8 245850922/78256779 -? 16.927 ns 0.4576 ns 0.0251 ns 1.00 0.00 - - NA
CompareTo ShortRun-.NET 8.0 .NET 8.0 245850922/78256779 0 8.078 ns 0.6699 ns 0.0367 ns 1.00 0.00 - - NA
CompareToV1 ShortRun-.NET 8.0 .NET 8.0 245850922/78256779 0 7.904 ns 1.6707 ns 0.0916 ns 0.98 0.01 - - NA
CompareToV2 ShortRun-.NET 8.0 .NET 8.0 245850922/78256779 0 7.445 ns 0.9916 ns 0.0544 ns 0.92 0.01 - - NA
CompareTo ShortRun-.NET Framework 4.8 .NET Framework 4.8 245850922/78256779 0 17.446 ns 0.5911 ns 0.0324 ns 1.00 0.00 - - NA
CompareToV1 ShortRun-.NET Framework 4.8 .NET Framework 4.8 245850922/78256779 0 24.015 ns 2.5017 ns 0.1371 ns 1.38 0.01 - - NA
CompareToV2 ShortRun-.NET Framework 4.8 .NET Framework 4.8 245850922/78256779 0 16.909 ns 1.9760 ns 0.1083 ns 0.97 0.01 - - NA
CompareTo ShortRun-.NET 8.0 .NET 8.0 12345(...)00000 [36] 61728(...)00000 [34] 56.414 ns 21.5782 ns 1.1828 ns 1.00 0.00 0.0019 32 B 1.00
CompareToV1 ShortRun-.NET 8.0 .NET 8.0 12345(...)00000 [36] 61728(...)00000 [34] 45.835 ns 4.8222 ns 0.2643 ns 0.81 0.02 0.0048 80 B 2.50
CompareToV2 ShortRun-.NET 8.0 .NET 8.0 12345(...)00000 [36] 61728(...)00000 [34] 47.987 ns 1.8267 ns 0.1001 ns 0.85 0.02 0.0048 80 B 2.50
CompareTo ShortRun-.NET Framework 4.8 .NET Framework 4.8 12345(...)00000 [36] 61728(...)00000 [34] 114.620 ns 4.3860 ns 0.2404 ns 1.00 0.00 0.0114 72 B 1.00
CompareToV1 ShortRun-.NET Framework 4.8 .NET Framework 4.8 12345(...)00000 [36] 61728(...)00000 [34] 122.911 ns 11.1478 ns 0.6110 ns 1.07 0.00 0.0126 80 B 1.11
CompareToV2 ShortRun-.NET Framework 4.8 .NET Framework 4.8 12345(...)00000 [36] 61728(...)00000 [34] 123.990 ns 22.5090 ns 1.2338 ns 1.08 0.01 0.0126 80 B 1.11
CompareTo ShortRun-.NET 8.0 .NET 8.0 97/1 89/1 8.285 ns 0.6389 ns 0.0350 ns 1.00 0.00 - - NA
CompareToV1 ShortRun-.NET 8.0 .NET 8.0 97/1 89/1 8.023 ns 0.6765 ns 0.0371 ns 0.97 0.00 - - NA
CompareToV2 ShortRun-.NET 8.0 .NET 8.0 97/1 89/1 8.168 ns 0.1987 ns 0.0109 ns 0.99 0.01 - - NA
CompareTo ShortRun-.NET Framework 4.8 .NET Framework 4.8 97/1 89/1 17.641 ns 31.6359 ns 1.7341 ns 1.00 0.00 - - NA
CompareToV1 ShortRun-.NET Framework 4.8 .NET Framework 4.8 97/1 89/1 18.033 ns 1.4123 ns 0.0774 ns 1.03 0.09 - - NA
CompareToV2 ShortRun-.NET Framework 4.8 .NET Framework 4.8 97/1 89/1 17.793 ns 0.8971 ns 0.0492 ns 1.01 0.09 - - NA
CompareTo ShortRun-.NET 8.0 .NET 8.0 1000/1 100/1 8.220 ns 0.0608 ns 0.0033 ns 1.00 0.00 - - NA
CompareToV1 ShortRun-.NET 8.0 .NET 8.0 1000/1 100/1 8.923 ns 0.0082 ns 0.0004 ns 1.09 0.00 - - NA
CompareToV2 ShortRun-.NET 8.0 .NET 8.0 1000/1 100/1 8.033 ns 1.1407 ns 0.0625 ns 0.98 0.01 - - NA
CompareTo ShortRun-.NET Framework 4.8 .NET Framework 4.8 1000/1 100/1 16.493 ns 1.3629 ns 0.0747 ns 1.00 0.00 - - NA
CompareToV1 ShortRun-.NET Framework 4.8 .NET Framework 4.8 1000/1 100/1 17.786 ns 0.2462 ns 0.0135 ns 1.08 0.01 - - NA
CompareToV2 ShortRun-.NET Framework 4.8 .NET Framework 4.8 1000/1 100/1 17.752 ns 0.9151 ns 0.0502 ns 1.08 0.01 - - NA

And here are the charts with your results:
First- the absolute values of the CompareTo method with the data from your machine (note that there are 2 jobs per framework- I assume you must have used additional startup parameters which has doubled the number of results):

image

For the differences I've merged the two runs- here we're seeing the summed-up difference in nanoseconds between CompareTo and CompareToV1:

image

And here is CompareTo vs CompareToV2 (the sum of difference between the two runs):

image

@lipchev
Copy link
Contributor Author

lipchev commented Jun 27, 2024

I cannot tell which algorithm is the "winner".

Looking at the results I see just one or two values for which CompareTo is loosing:

  1. When comparing with NaN CompareToV1 is faster but that would be at the expense of the more common case of having equal denominators.
  2. Comparing 12.3456789987654321m and 12.3456789987654322m - I think this is the only case where we actually end up performing all operations of the CompareTo method- which makes it slower in terms of the Mean (at least on .NET8- you're results are actually showing it as slower on .NET Framework) - however in both cases it ends up with less allocations.

I also feel that the benchmark must not only focus on the edge cases (or "extreme values"). Which numbers are likely? And what magnitude are these numbers?

I'm not sure there is a straightforward answer here- if you ask me what is the most common values that would be compared, from the numbers we have - I'd say something like 135/1000 and 76/1000 would be the most common ones. These, as well as the integers, fall into the category of equal denominators - which is handled the same way in all implementations.

Now let's consider the remaining cases (I'm of course discounting the Zero/NaN/Infinity): if the denominators are different, then CompareTo has the advantage of being able taking an early off-ramp, when comparing different values, but in the case where the two values are equal (or very close to one another) and both the straight multiplications and the reduced-multiplications end up with a similarly sized result - then doing the extra hoops would be in vain.
In fact- I can already predict what values are expected to yield the smallest performance ratio: small equal values such as 10/10 and 100/100 where the straight multiplication does not exceed the Int.MaxValue:

Method Job Runtime a b Mean Error StdDev Ratio RatioSD Allocated Alloc Ratio
CompareTo ShortRun-.NET 8.0 .NET 8.0 77/3600 77/3600 8.013 ns 0.8391 ns 0.0460 ns 1.00 0.00 - NA
CompareToV1 ShortRun-.NET 8.0 .NET 8.0 77/3600 77/3600 8.922 ns 0.3321 ns 0.0182 ns 1.11 0.01 - NA
CompareToV2 ShortRun-.NET 8.0 .NET 8.0 77/3600 77/3600 8.006 ns 0.3543 ns 0.0194 ns 1.00 0.01 - NA
CompareTo ShortRun-.NET Framework 4.8 .NET Framework 4.8 77/3600 77/3600 10.871 ns 1.5414 ns 0.0845 ns 1.00 0.00 - NA
CompareToV1 ShortRun-.NET Framework 4.8 .NET Framework 4.8 77/3600 77/3600 18.065 ns 2.6464 ns 0.1451 ns 1.66 0.01 - NA
CompareToV2 ShortRun-.NET Framework 4.8 .NET Framework 4.8 77/3600 77/3600 17.757 ns 1.7874 ns 0.0980 ns 1.63 0.02 - NA
CompareTo ShortRun-.NET 8.0 .NET 8.0 1350/10000 135/1000 26.106 ns 1.4403 ns 0.0789 ns 1.00 0.00 - NA
CompareToV1 ShortRun-.NET 8.0 .NET 8.0 1350/10000 135/1000 21.150 ns 1.1627 ns 0.0637 ns 0.81 0.00 - NA
CompareToV2 ShortRun-.NET 8.0 .NET 8.0 1350/10000 135/1000 21.283 ns 0.7969 ns 0.0437 ns 0.82 0.00 - NA
CompareTo ShortRun-.NET Framework 4.8 .NET Framework 4.8 1350/10000 135/1000 64.616 ns 7.5431 ns 0.4135 ns 1.00 0.00 - NA
CompareToV1 ShortRun-.NET Framework 4.8 .NET Framework 4.8 1350/10000 135/1000 56.269 ns 1.9701 ns 0.1080 ns 0.87 0.01 - NA
CompareToV2 ShortRun-.NET Framework 4.8 .NET Framework 4.8 1350/10000 135/1000 65.254 ns 13.8542 ns 0.7594 ns 1.01 0.01 - NA
CompareTo ShortRun-.NET 8.0 .NET 8.0 70742(...)85248 [33] 70742(...)85248 [33] 10.985 ns 0.2233 ns 0.0122 ns 1.00 0.00 - NA
CompareToV1 ShortRun-.NET 8.0 .NET 8.0 70742(...)85248 [33] 70742(...)85248 [33] 10.837 ns 0.7711 ns 0.0423 ns 0.99 0.00 - NA
CompareToV2 ShortRun-.NET 8.0 .NET 8.0 70742(...)85248 [33] 70742(...)85248 [33] 11.450 ns 1.0993 ns 0.0603 ns 1.04 0.00 - NA
CompareTo ShortRun-.NET Framework 4.8 .NET Framework 4.8 70742(...)85248 [33] 70742(...)85248 [33] 17.641 ns 1.7730 ns 0.0972 ns 1.00 0.00 - NA
CompareToV1 ShortRun-.NET Framework 4.8 .NET Framework 4.8 70742(...)85248 [33] 70742(...)85248 [33] 23.002 ns 2.7869 ns 0.1528 ns 1.30 0.00 - NA
CompareToV2 ShortRun-.NET Framework 4.8 .NET Framework 4.8 70742(...)85248 [33] 70742(...)85248 [33] 22.850 ns 0.7851 ns 0.0430 ns 1.30 0.00 - NA
CompareTo ShortRun-.NET 8.0 .NET 8.0 12345(...)00000 [36] 12345(...)00000 [36] 10.811 ns 0.6130 ns 0.0336 ns 1.00 0.00 - NA
CompareToV1 ShortRun-.NET 8.0 .NET 8.0 12345(...)00000 [36] 12345(...)00000 [36] 10.731 ns 1.6391 ns 0.0898 ns 0.99 0.01 - NA
CompareToV2 ShortRun-.NET 8.0 .NET 8.0 12345(...)00000 [36] 12345(...)00000 [36] 11.884 ns 0.9202 ns 0.0504 ns 1.10 0.01 - NA
CompareTo ShortRun-.NET Framework 4.8 .NET Framework 4.8 12345(...)00000 [36] 12345(...)00000 [36] 21.918 ns 1.0947 ns 0.0600 ns 1.00 0.00 - NA
CompareToV1 ShortRun-.NET Framework 4.8 .NET Framework 4.8 12345(...)00000 [36] 12345(...)00000 [36] 23.104 ns 1.6760 ns 0.0919 ns 1.05 0.00 - NA
CompareToV2 ShortRun-.NET Framework 4.8 .NET Framework 4.8 12345(...)00000 [36] 12345(...)00000 [36] 22.790 ns 0.5811 ns 0.0319 ns 1.04 0.00 - NA
CompareTo ShortRun-.NET 8.0 .NET 8.0 9710/100 971/10 27.222 ns 1.1951 ns 0.0655 ns 1.00 0.00 - NA
CompareToV1 ShortRun-.NET 8.0 .NET 8.0 9710/100 971/10 21.060 ns 0.4531 ns 0.0248 ns 0.77 0.00 - NA
CompareToV2 ShortRun-.NET 8.0 .NET 8.0 9710/100 971/10 21.466 ns 1.8394 ns 0.1008 ns 0.79 0.00 - NA
CompareTo ShortRun-.NET Framework 4.8 .NET Framework 4.8 9710/100 971/10 64.514 ns 2.0523 ns 0.1125 ns 1.00 0.00 - NA
CompareToV1 ShortRun-.NET Framework 4.8 .NET Framework 4.8 9710/100 971/10 56.302 ns 4.4721 ns 0.2451 ns 0.87 0.00 - NA
CompareToV2 ShortRun-.NET Framework 4.8 .NET Framework 4.8 9710/100 971/10 56.128 ns 3.6330 ns 0.1991 ns 0.87 0.00 - NA
CompareTo ShortRun-.NET 8.0 .NET 8.0 1000/10 100/1 20.237 ns 0.4223 ns 0.0231 ns 1.00 0.00 - NA
CompareToV1 ShortRun-.NET 8.0 .NET 8.0 1000/10 100/1 14.905 ns 0.0221 ns 0.0012 ns 0.74 0.00 - NA
CompareToV2 ShortRun-.NET 8.0 .NET 8.0 1000/10 100/1 15.120 ns 0.7832 ns 0.0429 ns 0.75 0.00 - NA
CompareTo ShortRun-.NET Framework 4.8 .NET Framework 4.8 1000/10 100/1 42.327 ns 3.1127 ns 0.1706 ns 1.00 0.00 - NA
CompareToV1 ShortRun-.NET Framework 4.8 .NET Framework 4.8 1000/10 100/1 40.092 ns 5.6520 ns 0.3098 ns 0.95 0.01 - NA
CompareToV2 ShortRun-.NET Framework 4.8 .NET Framework 4.8 1000/10 100/1 39.751 ns 0.6492 ns 0.0356 ns 0.94 0.00 - NA

From the opposite point of view, CompareTo would have the largest performance benefit when comparing numbers with different denominator, where the straight-multiplication ends up crossing the int.MaxInteger threshold, while CompareTo doesn't (ideally taking an early off-ramp, such as when comparing numbers with opposite signs).

Finally, if we're only working with the non-reduced fractions (as I would recommend) - then the concept of small numbers goes out the window the moment we hit a few multiplications: even a simple expression such as (135/1000 * 76/1000 + 77/3600) * 9710/100 should give us something like 747549386600/10000000000 (the AI did the math, I haven't checked) which is probably within one straight multiplication away from hitting the non-trivial threshold..

@lipchev
Copy link
Contributor Author

lipchev commented Jun 27, 2024

a simple expression such as (135/1000 * 76/1000 + 77/3600) * 9710/100 should give us something like 747549386600/10000000000 (the AI did the math, I haven't checked) which is probably within one straight multiplication away from hitting the non-trivial threshold..

Ok, clearly my agent has skipped some math classes- but I think the reasoning still holds.. 😄

@danm-de
Copy link
Owner

danm-de commented Jul 1, 2024

Even if the battle is “only” for a few nanoseconds: I would now merge the pull request into the master branch.

I'll be honest - the algorithm is not easy to understand right away and requires explanation. I hope my added code comments will be helpful in some way to future-danm and future-lipchev 🥲

@danm-de danm-de merged commit 46e8c05 into danm-de:master Jul 1, 2024
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants