fsharp / fsharp/fslang-suggestions
`ReadOnlySpan` initialization from static data
- Dominant language
- No language data
- Stars
- 373
- Forks
- 21
- PR merge metrics
- No merged PRs in 30d
Description
Compare https://github.com/dotnet/csharplang/issues/5295 and https://github.com/dotnet/roslyn/pull/24621, ultimately subsumed by https://github.com/dotnet/csharplang/issues/5354.
I propose that we add compiler support for translating
```fsharp
let data = ReadOnlySpan [|constant; values|]
```
```fsharp
let data = ReadOnlySpan "abc123"B
```
=
```fsharp
let data = new ReadOnlySpan<_> [|constant; values|]
```
```fsharp
let data = new ReadOnlySpan "abc123"B
```
and/or
```fsharp
let data = [|constant; values|].AsSpan ()
```
```fsharp
let data = "abc123"B.AsSpan ()
```
and/or, potentially (this would imply some kind of invocation of `op_Implicit` or else type-direction à la #1086)
```fsharp
let data : ReadOnlySpan<_> = [|constant; values|]
```
```fsharp
let data : ReadOnlySpan = "abc123"B
```
to non-allocating code that bakes the constant data into the assembly as a binary blob and creates a `ReadOnlySpan` at runtime that points to that data.
C# has done this for a while for
```csharp
public static ReadOnlySpan Data => new byte[] { constant, values };
```
and now does it for collection expressions as well ([SharpLab](https://sharplab.io/#v2:D4AQTAjAsAUCDMACciDCiDetE+UkEAbIgEoCmAhgCYDyAdgDYCeAygA4V0A8ARkwC5kAfIgBCAsgGdEAXhEBtCABpEYFfAC6Abmy5dOBMiKI+gxAFkAFAEpM+3DnLV6zdp14SRpqbMSKVaoiaOjAODiAA7CYSkora9gC+sAlAA==)):
```csharp
public static ReadOnlySpan Bytes => [1, 2, 3];
public static byte M() {
ReadOnlySpan bytes = [1, 2, 3];
return bytes[1];
}
```
```csharp
using System;
using System.Diagnostics;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Security;
using System.Security.Permissions;
[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)]
[assembly: AssemblyVersion("0.0.0.0")]
[module: UnverifiableCode]
[module: RefSafetyRules(11)]
public class C
{
public unsafe static ReadOnlySpan Bytes
{
get
{
return new ReadOnlySpan(Unsafe.AsPointer(ref .039058C6F2C0CB492C533B0A4D14EF77CC0F78ABCCCED5287D84A1A2011CFB81), 3);
}
}
public unsafe static byte M()
{
return new ReadOnlySpan(Unsafe.AsPointer(ref .039058C6F2C0CB492C533B0A4D14EF77CC0F78ABCCCED5287D84A1A2011CFB81), 3)[1];
}
}
[CompilerGenerated]
internal sealed class
{
[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 3)]
private struct __StaticArrayInitTypeSize=3
{
}
internal static readonly __StaticArrayInitTypeSize=3 039058C6F2C0CB492C533B0A4D14EF77CC0F78ABCCCED5287D84A1A2011CFB81/* Not supported: data(01 02 03) */;
}
```
The F# compiler is essentially already halfway there for statically-initialized arrays — it [emits static blobs holding the data](https://github.com/dotnet/fsharp/blob/096ac344bb2a1642cd12778b30b26cb6c3363d4a/src/Compiler/CodeGen/IlxGen.fs#L3600-L3703), but a new array is created and initialized with the data from the blob at runtime ([SharpLab](https://sharplab.io/#v2:DYLgZgzgPg9gDgUwHYAIDKBPCAXBBbAWAChjgFsUATAQ22pQF4UBtKARgFcMBuFAJi68AzFygBdYqXIowKABQBKRsRSqUZCjTqMW7Qf30iM4lWq3VmbMUA==)).
```fsharp
let data = [|1uy; 2uy; 3uy|]
let f () =
let data = [|1uy; 2uy; 3uy|]
data[1]
```
```csharp
using System;
using System.Diagnostics;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using ;
using Microsoft.FSharp.Core;
[assembly: FSharpInterfaceDataVersion(2, 0, 0)]
[assembly: AssemblyVersion("0.0.0.0")]
[CompilationMapping(SourceConstructFlags.Module)]
public static class @_
{
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
internal static .T5291936_3Bytes@ field5291937@/* Not supported: data(01 02 03) */;
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
internal static .T5291936_3Bytes@ field5291938@/* Not supported: data(01 02 03) */;
[CompilationMapping(SourceConstructFlags.Value)]
public static byte[] data
{
get
{
return $_.data@3;
}
}
public static byte f()
{
byte[] array = new byte[3];
RuntimeHelpers.InitializeArray(array, (RuntimeFieldHandle)/*OpCode not supported: LdMemberToken*/);
return array[1];
}
static _()
{
$_.init@ = 0;
int init@ = $_.init@;
}
}
namespace
{
internal static class $_
{
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
internal static readonly byte[] data@3;
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
[CompilerGenerated]
[DebuggerNonUserCode]
internal static int init@;
static $_()
{
byte[] array = new byte[3];
RuntimeHelpers.InitializeArray(array, (RuntimeFieldHandle)/*OpCode not supported: LdMemberToken*/);
data@3 = array;
}
}
}
internal static class
{
[StructLayout(LayoutKind.Explicit, Size = 3)]
internal struct T5291936_3Bytes@
{
}
}
```
The proposal here is to update the compiler to emit code for accessing the static data directly via a `ReadOnlySpan`, with no runtime array allocation or initialization, when the static array is directly bound or consumed as a `ReadOnlySpan`.
## Pros and Cons
**The advantages of making this adjustment to F# are**
- It adds a super-efficient way to access constant collections of data.
**The disadvantages of making this adjustment to F# are**
- It doesn't seem at first glance that this would have any ill effects. For example, making this change work for array literals `[|…|]` doesn't preclude later making it also work with type-directed `[…]`. All the rules about how the resulting span can be used are already in place and would still apply.
## Extra information
**Estimated cost (XS, S, M, L, XL, XXL):**
- S, unless we wanted to get into #1086 territory, in which case XL+...
**Related suggestions:**
- Potentially #1086.
## Affidavit (please submit!)
Please tick these items by placing a cross in the box:
* [x] This is not a question (e.g. like one you might ask on [StackOverflow](http://stackoverflow.com)) and I have searched StackOverflow for discussions of this issue
* [x] This is a language change and not purely a tooling change (e.g. compiler bug, editor support, warning/error messages, new warning, non-breaking optimisation) belonging to [the compiler and tooling repository](https://github.com/dotnet/fsharp)
* [x] This is not something which has obviously "already been decided" in previous versions of F#. If you're questioning a fundamental design decision that has obviously already been taken (e.g. "Make F# untyped") then please don't submit it
* [x] I have [searched both open and closed suggestions on this site](http://github.com/fsharp/fslang-suggestions/issues) and believe this is not a duplicate
Please tick all that apply:
* [x] This is not a breaking change to the F# language design
* [x] I or my company would be willing to help implement and/or test this
## For Readers
If you would like to see this issue implemented, please click the :+1: emoji on this issue. These counts are used to generally order the suggestions by engagement.
Contributor guide
No contributing guide indexed for this repository
Research direction
Start with the static-array emission logic in src/Compiler/CodeGen/IlxGen.fs, especially the linked section around lines 3600–3703, and compare it with the issue's C# and F# examples. Review the related language-design issues and determine which proposed syntax and cases are in scope. Done means constant array data is accessed through a ReadOnlySpan without runtime array allocation or initialization.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- fsharp
- Domain
- compilers
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Stale
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100