Array/Span access performance regression
- Dominant language
- C#
- Stars
- 18.3k
- Forks
- 5.6k
- PR merge metrics
- PR metrics pending
Description
### Description
We are doing a lot of array operations on very large arrays. Comparing multiple benchmarks from .NET8, .NET9, .NET10 shows that .NET8 is more stable and consistent in the performance results whereas starting with .NET9. things have gotten worse.
```
.NET8
| Method | Mean | Error | StdDev | Allocated |
|------------------------------ |---------:|----------:|----------:|----------:|
| Iterate_With_Array | 1.577 ms | 0.0252 ms | 0.0224 ms | - |
| Iterate_With_Span | 1.583 ms | 0.0313 ms | 0.0277 ms | - |
| Iterate_With_SpanWrapper | 1.557 ms | 0.0310 ms | 0.0369 ms | - |
| Iterate_With_ArrayWrapper | 1.572 ms | 0.0286 ms | 0.0469 ms | - |
| Iterate_With_ArrayWrapperPoor | 1.576 ms | 0.0213 ms | 0.0189 ms | - |
.NET9
| Method | Mean | Error | StdDev | Allocated |
|------------------------------ |---------:|----------:|----------:|----------:|
| Iterate_With_Array | 1.588 ms | 0.0233 ms | 0.0218 ms | - |
| Iterate_With_Span | 1.583 ms | 0.0157 ms | 0.0139 ms | - |
| Iterate_With_SpanWrapper | 1.567 ms | 0.0187 ms | 0.0175 ms | - |
| Iterate_With_ArrayWrapper | 1.753 ms | 0.0259 ms | 0.0242 ms | - |
| Iterate_With_ArrayWrapperPoor | 1.989 ms | 0.0378 ms | 0.0354 ms | - |
.NET10
| Method | Mean | Error | StdDev | Allocated |
|------------------------------ |---------:|----------:|----------:|----------:|
| Iterate_With_Array | 1.536 ms | 0.0298 ms | 0.0264 ms | - |
| Iterate_With_Span | 1.710 ms | 0.0339 ms | 0.0751 ms | - |
| Iterate_With_SpanWrapper | 1.628 ms | 0.0322 ms | 0.0644 ms | - |
| Iterate_With_ArrayWrapper | 1.450 ms | 0.0147 ms | 0.0123 ms | - |
| Iterate_With_ArrayWrapperPoor | 1.809 ms | 0.0144 ms | 0.0121 ms | - |
```
### Configuration
```
BenchmarkDotNet v0.15.8, Windows 11 (10.0.22631.6345/23H2/2023Update/SunValley3)
Intel Xeon Silver 4116 CPU 2.10GHz, 1 CPU, 24 logical and 12 physical cores
.NET SDK 10.0.101
[Host] : .NET 8.0.22 (8.0.22, 8.0.2225.52707), X64 RyuJIT x86-64-v4
DefaultJob : .NET 8.0.22 (8.0.22, 8.0.2225.52707), X64 RyuJIT x86-64-v4
```
### Regression?
It seems that .NET8 behaves more consistent.
### Data
Here's the benchmark code.
```cs
using BenchmarkDotNet.Attributes;
using System;
namespace span;
[MemoryDiagnoser]
public class Benchmarks
{
const int N = 1000000;
float[] _data;
[GlobalSetup]
public void Setup()
{
_data = new float[N];
for (int i = 0; i < N; i++) _data[i] = 1;
}
[Benchmark]
public double Iterate_With_Array()
{
double result = 0;
for (int i = 0; i < N; i++)
{
result += _data[i];
}
return result;
}
[Benchmark]
public double Iterate_With_Span()
{
double result = 0;
Span span = _data;
for (int i = 0; i < N; i++)
{
result += span[i];
}
return result;
}
[Benchmark]
public double Iterate_With_SpanWrapper()
{
double result = 0;
SpanWrapper span = new(_data);
for (int i = 0; i < N; i++)
{
result += span[i];
}
return result;
}
[Benchmark]
public double Iterate_With_ArrayWrapper()
{
double result = 0;
ArrayWrapper span = new(_data);
for (int i = 0; i < N; i++)
{
result += span[i];
}
return result;
}
[Benchmark]
public double Iterate_With_ArrayWrapperPoor()
{
double result = 0;
ArrayWrapperPoor span = new(_data);
for (int i = 0; i < N; i++)
{
result += span[i];
}
return result;
}
}
public readonly ref struct SpanWrapper
{
private readonly Span _span;
public SpanWrapper(Span span)
{
_span = span;
}
public ref T this[int index]
=> ref _span[index];
}
public readonly ref struct ArrayWrapper(T[] array)
{
public ref T this[int index]
=> ref array[index];
}
public readonly ref struct ArrayWrapperPoor(T[] array)
{
public ref T this[int index]
=> ref array.AsSpan()[index];
}
```
### Analysis
It seems there is a different codegen when using disasmo, but haven't inspected deeper
Contributor guide
Assessment
This issue has not been assessed yet.