JIT emits an array access bounds check during the value update via the for
Nobody has claimed this yet.
Assessment
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Newbie friendliness
- 35/100
- Issue type
- Bug
- Clarity
- Mostly clear
- Activity status
- Stale
- Tech stack
- csharp
- Domain
- compilers, performance
Research direction
Start with the ArrayBenchmarks.For and ForEachRef methods and run the BenchmarkDotNet command shown in the issue on .NET 9 and .NET 10. Compare the generated disassembly and performance; done means the ordinary array for loop no longer emits the additional bounds check and approaches the foreach ref code generation without regressing the reported cases.
Written by the indexing model from the issue text.
Description
Description
One of the familiar ways to update values in an array is to use a standard for loop. Currently, JIT generates an additional bounds check even for obvious variants (tests and details below).
for (var index = 0; index < _testArray.Length; index++)
{
_testArray[index] = arg;
}
To avoid this, you can use this approach:
foreach (ref var x in _testArray.AsSpan())
{
x = arg;
}
In my tests, the performance difference is 3..5% for .NET 10, and 13..23% for .NET 9.
.NET 10 has a lot of improvements in the code generation, but maybe there is a way to make it almost equivalent for and foreach ref?
Benchmark code
// To test .NET 10 Preview 2 used BenchmarkDotNet v0.14.1-nightly.
// To test .NET 9 used BenchmarkDotNet v0.14.0
// dotnet run -c Release -f net9.0 --filter "*" --runtimes net10.0
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
BenchmarkSwitcher.FromAssembly(typeof(ArrayBenchmarks).Assembly).Run(args);
[MinIterationCount(15)]
[HideColumns("Job", "Error", "StdDev", "RatioSD", "arg")]
[MeanColumn, MinColumn, MaxColumn, OperationsPerSecond]
[JsonExporter]
[DisassemblyDiagnoser]
public class ArrayBenchmarks
{
private int[] _testArray = null!;
[Params(10_000, 100_000, 1_000_000)]
public int Length { get; set; }
[GlobalSetup]
public void Setup()
{
_testArray = new int[Length];
var rnd = new Random(12345);
foreach (ref var x in _testArray.AsSpan())
{
x = rnd.Next();
}
}
[Benchmark(Baseline = true)]
[Arguments(42)]
public int For(int arg)
{
for (var index = 0; index < _testArray.Length; index++)
{
_testArray[index] = arg;
}
return _testArray.Length;
}
[Benchmark]
[Arguments(42)]
public int ForEachRef(int arg)
{
foreach (ref var x in _testArray.AsSpan())
{
x = arg;
}
return _testArray.Length;
}
}
Configuration
.NET 10 benchmark:
BenchmarkDotNet v0.14.1-nightly.20250107.205, Windows 10 (10.0.19045.5608/22H2/2022Update)
12th Gen Intel Core i9-12900K 3.19GHz, 1 CPU, 24 logical and 16 physical cores
.NET SDK 10.0.100-preview.2.25164.34
.NET 9 benchmark:
BenchmarkDotNet v0.14.0, Windows 10 (10.0.19045.5608/22H2/2022Update)
12th Gen Intel Core i9-12900K, 1 CPU, 24 logical and 16 physical cores
.NET SDK 10.0.100-preview.2.25164.34
Regression?
No
Data
.NET 10 Preview 2
| Method | Length | Mean | Min | Max | Op/s | Ratio | Code Size |
|---|---|---|---|---|---|---|---|
| For | 10000 | 2.001 μs | 1.937 μs | 2.059 μs | 499,853.1 | 1.00 | 51 B |
| ForEachRef | 10000 | 1.930 μs | 1.904 μs | 1.972 μs | 518,158.8 | 0.97 | 59 B |
| For | 100000 | 20.071 μs | 20.009 μs | 20.195 μs | 49,823.2 | 1.00 | 51 B |
| ForEachRef | 100000 | 19.016 μs | 18.991 μs | 19.073 μs | 52,586.4 | 0.95 | 59 B |
| For | 1000000 | 200.654 μs | 199.976 μs | 202.221 μs | 4,983.7 | 1.00 | 51 B |
| ForEachRef | 1000000 | 192.151 μs | 189.928 μs | 196.704 μs | 5,204.2 | 0.96 | 59 B |
ArrayBenchmarks-report-NET10.json
.NET 9
| Method | Length | Mean | Min | Max | Op/s | Ratio | Code Size |
|---|---|---|---|---|---|---|---|
| For | 10000 | 2.400 μs | 2.255 μs | 2.489 μs | 416,702.5 | 1.00 | 51 B |
| ForEachRef | 10000 | 1.914 μs | 1.905 μs | 1.940 μs | 522,468.1 | 0.80 | 61 B |
| For | 100000 | 25.069 μs | 24.286 μs | 26.037 μs | 39,890.6 | 1.00 | 51 B |
| ForEachRef | 100000 | 19.329 μs | 18.991 μs | 19.761 μs | 51,735.8 | 0.77 | 61 B |
| For | 1000000 | 218.993 μs | 211.513 μs | 223.108 μs | 4,566.4 | 1.00 | 51 B |
| ForEachRef | 1000000 | 190.688 μs | 190.107 μs | 191.467 μs | 5,244.2 | 0.87 | 61 B |
ArrayBenchmarks-report-NET9.json
Analysis
for codegen:
; ArrayBenchmarks.For(Int32)
sub rsp,28
xor eax,eax
mov rcx,[rcx+8]
cmp dword ptr [rcx+8],0
jle short M00_L01
M00_L00:
mov r8,rcx
cmp eax,[r8+8]
jae short M00_L02
mov [r8+rax*4+10],edx
inc eax
cmp [rcx+8],eax
jg short M00_L00
M00_L01:
mov eax,[rcx+8]
add rsp,28
ret
M00_L02:
call CORINFO_HELP_RNGCHKFAIL
int 3
foreach ref codegen:
; ArrayBenchmarks.ForEachRef(Int32)
mov rax,[rcx+8]
test rax,rax
je short M00_L03
lea r8,[rax+10]
mov eax,[rax+8]
M00_L00:
test eax,eax
jle short M00_L02
xor r10d,r10d
nop word ptr [rax+rax]
M00_L01:
mov [r8+r10],edx
add r10,4
dec eax
jne short M00_L01
M00_L02:
mov rax,[rcx+8]
mov eax,[rax+8]
ret
M00_L03:
xor r8d,r8d
xor eax,eax
jmp short M00_L00
; Total bytes of code 59
- Dominant language
- C#
- Stars
- 18.3k
- Forks
- 5.6k
- PR merge metrics
- PR metrics pending
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from dotnet/runtime
-
agentic-workflows untriaged
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
-
area-System.Reflection blocking-clean-ci-optional Known Build Error os-mac-os-x untriaged
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
-
area-CodeGen-coreclr untriaged
Difficulty 1/5 Under an hour Newbie friendliness 92/100
-
agentic-workflows untriaged
Difficulty 1/5 Under an hour Newbie friendliness 78/100
-
area-VM-meta-mono untriaged
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
Similar issues
-
Difficulty 2/5 1-3 hours Newbie friendliness 86/100
-
:watch: Not Triaged 11.0 fundamentals/subsvc
Difficulty 2/5 1-3 hours Newbie friendliness 92/100
dotnet/AspNetCore.Docs#37699 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
SubtitleEdit/subtitleedit#15108 · 1 comment ·
-
area/docs-content Bug pulumi/docs
Difficulty 1/5 1-3 hours Newbie friendliness 94/100
-
Create parent directories only after the containment check in InstallHelper.TryExtractToDirectory Open
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
PowerShell/PSResourceGet#2056 ·