I'm working with some benchmarks, and I noticed some odd behaviour with my methods vs. Linq's implementation. Testing on .NET 9, X64 RyuJIT.
I'm determining the max value within a decimal array. In testing, I wrote a simplified version of Linq's Max(decimal) method that removes the type-checking overhead. It's otherwise an exact copy from the source.
I expected the performance to basically be identical, but the odd thing is that my simplified version is slower than Linq's.
private decimal[] _array = default!;
[GlobalSetup]
public void GlobalSetup()
{
_array = new decimal[100];
for (int i = 0; i < _array.Length; i++)
_array[i] = (decimal)i;
}
[Benchmark]
public decimal MyMax()
{
ReadOnlySpan<decimal> span = _array;
if (span.IsEmpty)
throw new ArgumentException();
decimal value = span[0];
for (int i = 1; (uint)i < (uint)span.Length; i++)
{
if (span[i] > value)
{
value = span[i];
}
}
return value;
}
[Benchmark]
public decimal Linq()
{
return _array.Max();
}
Intel Core i7-7700 CPU 3.60GHz (Kaby Lake), 1 CPU, 8 logical and 4 physical cores
.NET SDK 9.0.304
[Host] : .NET 9.0.8 (9.0.825.36511), X64 RyuJIT AVX2
DefaultJob : .NET 9.0.8 (9.0.825.36511), X64 RyuJIT AVX2
| Method | Mean | Error | StdDev | Code Size | Allocated |
|------- |---------:|--------:|--------:|----------:|----------:|
| MyMax | 498.7 ns | 3.63 ns | 3.03 ns | 578 B | - |
| Linq | 446.2 ns | 4.48 ns | 3.97 ns | 759 B | - |
Just to ensure this wasn't some sort of JIT side effect, I tried putting the Linq method before MyMax so that Linq would be benchmarked first, and results were the same: Linq is still faster than MyMax.
I admit, I'm really bad at assembly, so I can't really determine anything looking at the disassembly. But, regardless, considering I'm using the same C# code as Linq, even if I was able to find what was different I still wouldn't know why it happened.
I know it's not a huge difference, but more than anything, I just really wonder, academically, why it's happening. Can someone enlighten me? I sure hope it's something simple and obvious I'm missing.
Thanks in advance!
I ran the benchmark using BenchmarkDotNet v0.15.2 on Windows 11 with .NET SDK 9.0.304 and a 13th Gen Intel Core i5-13500H. Here are the results I obtained:
Code:
public class Program
{
private decimal[] _array = default!;
[Params(10, 100, 1000)]
public int N;
[GlobalSetup(Targets = new[] { nameof(MyMax), nameof(MyMax2), nameof(Linq) })]
public void GlobalSetupWorst()
{
_array = new decimal[N];
for (int i = 0; i < _array.Length; i++) _array[i] = (decimal)i;
}
[GlobalSetup(Targets = new[] { nameof(MyMaxRand), nameof(MyMax2Rand), nameof(LinqRand) })]
public void GlobalSetupRand()
{
_array = new decimal[N];
var random = new Random(0);
for (int i = 0; i < _array.Length; i++) _array[i] = (decimal)(random.NextDouble() * _array.Length);
}
[Benchmark]
public decimal MyMax()
{
ReadOnlySpan<decimal> span = _array;
if (span.IsEmpty) throw new ArgumentException();
decimal value = span[0];
for (int i = 1; (uint)i < (uint)span.Length; i++)
{
if (span[i] > value)
{
value = span[i];
}
}
return value;
}
[Benchmark]
public decimal MyMaxRand()
{
ReadOnlySpan<decimal> span = _array;
if (span.IsEmpty) throw new ArgumentException();
decimal value = span[0];
for (int i = 1; (uint)i < (uint)span.Length; i++)
{
if (span[i] > value)
{
value = span[i];
}
}
return value;
}
[Benchmark]
public decimal MyMax2()
{
ReadOnlySpan<decimal> span = _array;
if (span.IsEmpty) throw new ArgumentException();
decimal value = span[0];
foreach (var item in span)
{
if (item > value)
{
value = item;
}
}
return value;
}
[Benchmark]
public decimal MyMax2Rand()
{
ReadOnlySpan<decimal> span = _array;
if (span.IsEmpty) throw new ArgumentException();
decimal value = span[0];
foreach (var item in span)
{
if (item > value)
{
value = item;
}
}
return value;
}
[Benchmark]
public decimal Linq()
{
return _array.Max();
}
[Benchmark]
public decimal LinqRand()
{
return _array.Max();
}
public static void Main(string[] args)
{
var summary = BenchmarkRunner.Run(typeof(Program).Assembly);
}
}
results:
| Method | N | Mean | Error | StdDev |
|----------- |----- |------------:|----------:|----------:|
| MyMax | 10 | 19.45 ns | 0.218 ns | 0.421 ns |
| MyMaxRand | 10 | 24.75 ns | 0.165 ns | 0.155 ns |
| MyMax2 | 10 | 22.38 ns | 0.204 ns | 0.191 ns |
| MyMax2Rand | 10 | 29.50 ns | 0.259 ns | 0.243 ns |
| Linq | 10 | 21.43 ns | 0.335 ns | 0.279 ns |
| LinqRand | 10 | 25.76 ns | 0.226 ns | 0.212 ns |
| MyMax | 100 | 240.25 ns | 3.708 ns | 4.271 ns |
| MyMaxRand | 100 | 241.15 ns | 2.617 ns | 2.448 ns |
| MyMax2 | 100 | 245.91 ns | 2.030 ns | 1.899 ns |
| MyMax2Rand | 100 | 252.84 ns | 2.138 ns | 1.785 ns |
| Linq | 100 | 252.23 ns | 2.082 ns | 1.948 ns |
| LinqRand | 100 | 241.05 ns | 2.906 ns | 2.718 ns |
| MyMax | 1000 | 2,462.44 ns | 25.196 ns | 23.569 ns |
| MyMaxRand | 1000 | 2,639.48 ns | 24.629 ns | 23.038 ns |
| MyMax2 | 1000 | 2,391.85 ns | 15.602 ns | 13.830 ns |
| MyMax2Rand | 1000 | 2,589.29 ns | 20.012 ns | 18.719 ns |
| Linq | 1000 | 2,496.53 ns | 32.116 ns | 28.470 ns |
| LinqRand | 1000 | 2,480.43 ns | 23.355 ns | 21.846 ns |
While it's interesting to see these numbers, it's important to be cautious about interpreting microbenchmark results at the nanosecond scale—especially for simple operations.
Microbenchmarks (in the 20–2500 ns range) are often influenced more by factors like CPU caching, branch prediction, JIT optimizations, and even quirks of the benchmarking harness than by the actual code. On modern hardware, these effects can obscure real-world performance differences and make the results nearly meaningless for practical applications.
Differences of a few nanoseconds are unlikely to matter in a real system, and can easily fluctuate between runs. What matters much more is how your code behaves under realistic workloads, with real data, in your production environment.
TL;DR: Microbenchmarking is useful for learning, but don't take tiny time differences as a sign that one approach is “better” than another—especially for code running in the tens of nanoseconds. Focus on clarity, maintainability, and real-world profiling if you want to optimize for performance.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With