2

In C# Math.Max() is implemented as:

   [NonVersionable]
   public static int Max(int val1, int val2)
   {
       return (val1 >= val2) ? val1 : val2;
   }

While I believe that it can also be implemented as:

enter image description here

Does C#'s implementation have any benefit such as efficiency over implementering it mathemathicaly?

(a + b + Math.Abs(a-b)) /2

O S
  • 427
  • 1
  • 9
  • 4
    If they achieve the same result, what's the difference? [Performance?](https://github.com/dotnet/BenchmarkDotNet) Did you measure performance? And why reinvent the wheel? – mason Feb 07 '21 at 13:44
  • 3
    if you care about performance, have you tried _benchmarking_ it? – Franz Gleichmann Feb 07 '21 at 13:44
  • 2
    also, when handling floating point value, the mathematical method results in precision loss. – Franz Gleichmann Feb 07 '21 at 13:53
  • 1
    If you actually had correct code, it might be faster than the the official version due to branch prediction. But as others have mentioned, correct code is difficult – Charlieface Feb 07 '21 at 13:58
  • 2
    The actual 'Math.Max' implementation is simple, straightforward and bug free. The so called *mathematical* implementation is not obviously an implementation of max (it is, but readers have to think about it). The question is why would you twist your mind with this formula for a trivia like max? And that's because mathematicians doesn't like to work with conditional function, they are annoying to write and study. So if they can find a onliner they will use it even if it's a misrepresentation. Same applies with 'abs(x) = sqrt(x²)'. – Orace Feb 07 '21 at 16:16

4 Answers4

4

For one, the mathematical implementation is incorrect as it doesn't handle overflow/underflow.

If you do:

// suppose Max is implemented the "mathematical" way
Max(int.MaxValue, int.MaxValue - 1)

it would give -1.

Sweeper
  • 145,870
  • 17
  • 129
  • 225
2

Assuming your Math is correct and you could implement it like you proposed. It would still have zero to none benifit, because the time complexity would be O(1) in both cases. But I would argue that the implementation in C# is more readable. Moreover the Math.Abs function is porbably implemented like return x > 0 ? x : -x; which would be the same amount of conditionals as in the current implementation.

  • You haven't taken into account branch-misprediction on the `if` version – Charlieface Feb 07 '21 at 14:02
  • @Charlieface - that is taken into account by mentioning that `Math.Abs` probably has a branch anyway – Martin Smith Feb 07 '21 at 14:04
  • which it does. https://referencesource.microsoft.com/#mscorlib/system/math.cs,278 – Martin Smith Feb 07 '21 at 14:05
  • @MartinSmith Yeah I just looked that up before you posted. Yes and no. This answer doesn't really discuss it at all, you can maybe work it out by inference that by definition it must be slower. And if we're not allowing for edge cases of `MaxValue`, we can optimize `Abs` to `return 0x7FFFFFFF & val` – Charlieface Feb 07 '21 at 14:08
  • 1
    @MartinSmith Sorry correct implementation here https://stackoverflow.com/a/2074403/14868997 – Charlieface Feb 07 '21 at 14:26
0

Another reason would be dependent on your CPU. A CISC CPU is very like to have a max math operator. MAXSS in x64 for example.

A compiler might be able to figure out what you are trying to do. But with the workarounds necessary that seems highly unlikely. Allowing your intention to be pushed down the stack gives the best chance that your code resolves to a single CPU operation. I'm actually surprised by @Luuk's benchmark results. But it goes to show, optimize for readability, but when you care about performance make sure you measure thoroughly. In fact I try to always leave the performance tests behind in unit tests, b/c things change as compilers improve.

DevNull
  • 71
  • 6
-1

There was a comment about benchmarking, and a comment about overflow/underflow.

This code shows that the mathematical approach is quicker, even when doing a simplistic workaround for the overflow

    int a = 1;
    int b = Int32.MaxValue - 1;
    int count = 100000;
    DateTime start;
    Console.WriteLine($"a:{a} b:{b}");

    Console.WriteLine("Method1");
    start = DateTime.Now;
    for (int i=1; i< count; i++)
    {
        int c = Math.Max(a, b);
        //if (i == 1) Console.Write($"{c} ");
    }
    Console.WriteLine($"time: { (DateTime.Now - start).TotalMilliseconds }");


    Console.WriteLine("Method2");
    start = DateTime.Now;
    for (int i = 1; i < count; i++)
    {
        int c = Convert.ToInt32((double)(((double)a + b + Math.Abs(a - b)) / 2.0));
        //if(i == 1) Console.Write($"{c} ");
    }
    Console.WriteLine($"time: { (DateTime.Now - start).TotalMilliseconds }");

output:

a:1 b:2147483646
Method1
time: 3,0267
Method2
time: 0,4752
Press any key to continue . . .

EDIT: after suggestion to use benchmark.net

private readonly int a = 1;
private readonly int b = Int32.MaxValue - 1;

public Max()
{
    a = new Random(42).Next();
    b = new Random(24).Next();
}

[Benchmark]
public  int Method1() => Math.Max(a, b);

[Benchmark]
public int Method2() => Convert.ToInt32((double)(((double)a + b + Math.Abs(a - b)) / 2.0));

[Benchmark]
public int Method3() => ((a + b + Math.Abs(a - b) / 2));

[Benchmark]
public int Method4() => ((a >= b) ? a :b);

results:

// * Summary *

BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19042
Intel Core i7-8700K CPU 3.70GHz (Coffee Lake), 1 CPU, 12 logical and 6 physical cores
.NET Core SDK=5.0.102
  [Host]     : .NET Core 5.0.2 (CoreCLR 5.0.220.61120, CoreFX 5.0.220.61120), X64 RyuJIT
  DefaultJob : .NET Core 5.0.2 (CoreCLR 5.0.220.61120, CoreFX 5.0.220.61120), X64 RyuJIT


|  Method |      Mean |     Error |    StdDev |
|-------- |----------:|----------:|----------:|
| Method1 | 0.2288 ns | 0.0058 ns | 0.0054 ns |
| Method2 | 2.7820 ns | 0.0157 ns | 0.0123 ns |
| Method3 | 0.4558 ns | 0.0094 ns | 0.0088 ns |
| Method4 | 0.2645 ns | 0.0133 ns | 0.0124 ns |

// * Hints *
Outliers
  Max.Method2: Default -> 3 outliers were removed (4.21 ns..4.23 ns)

// * Legends *
  Mean   : Arithmetic mean of all measurements
  Error  : Half of 99.9% confidence interval
  StdDev : Standard deviation of all measurements
  1 ns   : 1 Nanosecond (0.000000001 sec)

// ***** BenchmarkRunner: End *****
// ** Remained 0 benchmark(s) to run **
Run time: 00:02:02 (122.05 sec), executed benchmarks: 4

Global total time: 00:02:05 (125.55 sec), executed benchmarks: 4
Luuk
  • 4,913
  • 3
  • 17
  • 21
  • 3
    "This code shows that the mathematical approach is quicker" - not in any rigorous way. I would strongly recommend using [benchmark.net](https://benchmarkdotnet.org/) instead of hand-rolling micro-benchmarks like this. (You may get the same result - but it would be *much* more compelling showing it as a benchmark.net example.) – Jon Skeet Feb 07 '21 at 14:21
  • 1
    This doesn't look like a good benchmark - reversing the order the two methods are tested in changes the results for me – Martin Smith Feb 07 '21 at 14:22
  • @JonSkeet: My coding leven in C# did not yet reach so far ... But with a factor of more than 6, I think I am on the safe side (3.0267/0.452=) – Luuk Feb 07 '21 at 14:24
  • 2
    Start by using `Stopwatch` before even thinking about benchmarking. – Charlieface Feb 07 '21 at 14:25
  • @Luuk - this is what I get when I reverse the order so you seem quite far from being on the "safe side" https://i.stack.imgur.com/MF28o.png – Martin Smith Feb 07 '21 at 14:28
  • 2
    @Luuk: You're measuring for a *tiny* amount of time - so no, that's *not* safe. For example, I wouldn't be surprised if what you're observing is basically re-jitting kicking in during the first loop. Microbenchmarking is hard, which is why benchmark.net exists. – Jon Skeet Feb 07 '21 at 14:31
  • OK, thanks all for the feedback on this.... I will have a look at benchmark.net. – Luuk Feb 07 '21 at 14:32
  • There is a lot of benefit to using trusted benchmarking tools like @JonSkeet mentioned. 1 of those is that everyone would trust the results a little more, there is still other factors at play like the test environment not always being 'ideal running conditions' and it's not on multiple platforms so maybe that differs too but 1 of the things a framework like benchmark.net does is warm ups to make sure the results are as evenly matched as possible, results for the above in the next comment code at https://github.com/Gordon-Beeming/Sample-Code/blob/main/src/TestMaxValue/TestMaxValue/Program.cs – Gordon Beeming Feb 07 '21 at 16:18
  • | Method | Mean | Error | StdDev | | CSharpWay | 0.0272 ns | 0.0011 ns | 0.0009 ns | | MathsWay | 0.0454 ns | 0.0008 ns | 0.0007 ns | | MathsWayWithFix | 3.3802 ns | 0.0032 ns | 0.0027 ns | – Gordon Beeming Feb 07 '21 at 16:18
  • Formatting of that was terrible, sorry. But in the case above if you just abstract the methods into methods and call them each twice, method1, method2, method1, method2 you'd get closer results to what a benchmarking framework does a:1 b:2147483646 Method1 time: 0.8578 Method2 time: 0.3466 Method1 time: 0.0522 Method2 time: 0.3589 – Gordon Beeming Feb 07 '21 at 16:19
  • 1
    Don't your revised tests contradict the assertion that "the mathematical approach is quicker"? 0.2288ns average vs 0.4558 ns? – Martin Smith Feb 07 '21 at 17:39