JIT emits an array access bounds check during the value update via the for

Open
#114,160 3 comments 1 reaction 0 assignees View on GitHub

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

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

area-CodeGen-coreclr tenet-performance
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

ArrayBenchmarks-asm-NET10.md


.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

ArrayBenchmarks-asm-NET9.md

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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from dotnet/runtime

All issues in dotnet/runtime

Similar issues

More C# issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.