fsharp / fsharp/fslang-suggestions

`ReadOnlySpan` initialization from static data

Open
#1,350 5 comments 8 reactions 0 assignees View on GitHub
approved-in-principle area: dotnet-codegen area: optimization
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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.