Computed ReadOnlySpan<T> properties always allocate RuntimeFieldInfoStubs in Debug Mode
- Dominant language
- C#
- Stars
- 18.3k
- Forks
- 5.6k
- PR merge metrics
- PR metrics pending
Description
### Description
I have come to notice that computed properties that return a hardcoded static buffer as a `ReadOnlySpan` always allocate a `RuntimeFieldInfoStub` on every access when compiling in debug mode.
```csharp
using System;
using System.Runtime.CompilerServices;
internal class Program
{
public static ReadOnlySpan Foo => [ 1, 2, 3, 4 ];
public static void Main(string[] args)
{
// Dummy code that does *something* with Foo.
var random = new Random();
for (int i = 0; i < 1000000; i++)
DoSomething(Foo[random.Next(Foo.Length)]);
}
private static void DoSomething(uint u)
{
// ...
}
}
```
After chatting with some of the folks over at the C# discord, it is my understanding that this happens because `Foo.get` gets compiled to a `ldtoken` plus a `call` to `System.Runtime.CompilerServices.RuntimeHelpers::CreateSpan`. This performs a resolution of a field handle to the underlying field storing the data, as well as a bunch of other checks (see also [SharpLab](https://sharplab.io/#v2:EYLgxg9gTgpgtADwGwBYA0AXEBLANmgExAGoAfAAQCYBGAWAChyAGAAnOoDoAlAVwDsM2ALYwOAYQhCADnhhQAynIBu2MDADOAbgYNsAuXwCGuNpTbUA7AwDeDFvbYBmc0jYoWAWUN6AFOyYA2gC6LIZQAObqAJR2Drb0DoksSmEsUIZ8BJIsALwsfDAA7ixcGVlCPlHaCUn2AGbQLD56GCzYuSxMmm0sADws1ExDw93YxMQxNbWJACIQ8pIwGAAWeuE+AGIQEAHpmZIcAHIwCBib2xwAMjB84StRQVWx9gC+OlMsAR5LyxAEAJLSXA+b4rP6AqS4ADyUkEED46iOEH+fFwejWD2eLCkUGwKQwMBcbhYcwWIhWax8PBaLB4k0S8USb3oWPIznYri4MEMBChqIAnvIpBletSBAA+FhbCC5SUBahoFiURWORUoILVF5AA=)). In Release mode, the JIT correctly optimizes away this call, but in Debug mode the allocation remains.
While I understand that, generally speaking, Debug builds are not meant to be optimized for performance nor are they meant to be profiled, I still feel this unnecessarily slows down a Debug build, especially if we consider these properties are really only there to expose some static read-only data. Right now, if I want to test my user application that uses a `ReadOnlySpan` property a lot (e.g., because it uses a lot of compile-time generated static data), your only options are to use a very slow Debug build, use a Release build that is unpredictable under a debugger, or rewrite the code that minimizes the direct accesses to the property (see considered alternatives below).
All options are not really great DX in my opinion. I was therefore wondering whether a specific case could be made to also optimize `ReadOnlySpan` properties with a static back buffer in Debug mode as well.
### Configuration
Tested on fresh installations of Ubuntu 25.04 x64, Windows 10 x64 and NixOS 25.11 x64, all with a fresh .NET 10 installation as well.
### Data
Running the code above in dotMemory reveals this data:
```
Allocated type : System.RuntimeFieldInfoStub
Objects : n/a
Bytes : 144000000
Allocated by
100% FromPtr • 137.33 MB / 137.33 MB • System.RuntimeFieldInfoStub.FromPtr(IntPtr)
100% get_Foo • 137.33 MB / - • global::Program.get_Foo()
100% Main • 137.33 MB / - • global::Program.Main(String[])
► 100% [AllThreadsRoot] • 137.33 MB / - • [AllThreadsRoot]
```
### Considered Alternatives
- I have considered replacing the `ReadOnlySpan` property with a `static readonly T[]` field specifically for debug builds
```csharp
public static readonly uint[] Foo = [ 1, 2, 3, 4 ];
```
but this of course loses the benefits of `ReadOnlySpan`.
- Alternatively, the `ReadOnlySpan` could be stored in a temp variable first;
```csharp
var foo = Foo;
for (int i = 0; i < 1000000; i++)
DoSomething(foo[random.Next(foo.Length)]);
```
but this loses some of the ergonomics on how you can use the property.
### Context
For context, this came up while trying to address some issues in AvaloniaUI/Avalonia#20175 where a debug build was significantly slower when rendering text (Avalonia uses many compile-time generated static buffers to do unicode-specific text formatting and rendering).
Contributor guide
Assessment
This issue has not been assessed yet.