FSharp.Core `#if NET` fast-path opportunities — SIMD / span / TensorPrimitives / formatting (unblocked by #20229)
- Dominant language
- F#
- Stars
- 4.3k
- Forks
- 876
- Avg merge
- 4d 11h
- Merged PRs (30d)
- 131
Description
[#20229](https://github.com/dotnet/fsharp/pull/20229) ships FSharp.Core with a `net10.0` TFM. A `#if NET8_0_OR_GREATER` / `#if NET` block therefore compiles **only** into the net asset — `netstandard2.0`/`netstandard2.1` stay bit-identical. This tracks the additive, guarded fast paths that unlocks.
**Important gate distinction** some span APIs are already in `netstandard2.1`, so FSharp.Core's ns2.1 asset could use them too — those are perf wins but **not** strictly gated on #20229.
Format per row: **F# affected** → **FSharp.Core file:line** → **.NET API** → guard → win.
## 1 · Numeric reductions → SIMD `TensorPrimitives` *(net-only)*
| F# affected | FSharp.Core file | .NET API | guard | win |
|---|---|---|---|---|
| `Array.sum` / `Array.average` | `array.fs:1628/1722` | `TensorPrimitives.Sum` (net8) | `#if NET8_0_OR_GREATER` | 2–5× · **gated**: float reassociation; int drops `Checked.(+)` overflow |
| `Array.max` / `Array.min` | `array.fs:1685/1648` | `TensorPrimitives.Max`/`Min` (net8) | `#if NET8_0_OR_GREATER` | 3–4× · **gated**: IEEE vs F# NaN — safe for ints |
| `Seq.sum/average/min/max` (array fast-path) | `seq.fs:1465/1485/1517/1557` | `TensorPrimitives` behind `:? ('T[])` (net8) | `#if NET8_0_OR_GREATER` | 2–4× · inherits above; array-backed only |
## 2 · Primitive-array equality & comparison *(ns2.1+)*
| F# affected | FSharp.Core file | .NET API | guard | win |
|---|---|---|---|---|
| `GenericEquality{Byte,Int32,Int64,Char}Array` | `prim-types.fs:1481/1496/1558/1543` | `MemoryExtensions.SequenceEqual<'T>` | `#if NETSTANDARD2_1_OR_GREATER || NET` | 40–70%, identical semantics |
| `GenericComparisonByteArray` | `prim-types.fs:1230` | `MemoryExtensions.SequenceCompareTo` | `#if NETSTANDARD2_1_OR_GREATER || NET` | 40–60% |
## 3 · Array search & reorder *(ns2.1+)*
| F# affected | FSharp.Core file | .NET API | guard | win |
|---|---|---|---|---|
| `Array.contains` (single primitive) | `array.fs:~488` | `MemoryExtensions.Contains` / `ReadOnlySpan.IndexOf` | `#if NETSTANDARD2_1_OR_GREATER || NET` | 2–10× |
| `Array.rev` | `array.fs:1386` | `System.Array.Reverse` | any TFM | small |
## 4 · Primitive-array hashing *(net-only)*
| F# affected | FSharp.Core file | .NET API | guard | win |
|---|---|---|---|---|
| `GenericHash{Byte,Int32,Int64}Array` | `prim-types.fs:964/975/986` | `HashCode.AddBytes` (net6) | `#if NET6_0_OR_GREATER` | 15–30% · keep in-process hash stability; 10-node cap |
## 5 · String building *(ns2.1+)*
| F# affected | FSharp.Core file | .NET API | guard | win |
|---|---|---|---|---|
| `String.map` / `String.mapi` / `String.filter` | `string.fs:57/71/89` | `String.Create(len, state, writer)` | `#if NETSTANDARD2_1_OR_GREATER || NET` | 1 pass, fewer allocs (`filter` needs a trim) |
## 6 · Formatting / printf *(mixed)*
| F# affected | FSharp.Core file | .NET API | guard | win |
|---|---|---|---|---|
| `%O`/`%P`/interpoland/user types/`decimal`/`DateTime` | `printf.fs:911–929` `ObjectToString`/`InterpolandToString` (+ span sink `1073–1083`) | `ISpanFormattable.TryFormat` (net6) + `String.Create` | `#if NET6_0_OR_GREATER` | skip 1 temp string per arg; polymorphic (no per-type match). No-width case first — padding is string-based today |
| numeric specifiers `%d %i %u %x %X %f %e %g %M` (no width/prec) | `printf.fs:746–770/852–859` | concrete `Int32/Int64/Double/Single/Decimal.TryFormat` into `Span` | `#if NETSTANDARD2_1_OR_GREATER || NET` | remove 1 temp string per hole when sink is span-capable |
| `StringBuilder` / `TextWriter` sinks | `printf.fs:1073–1083` | `StringBuilder.Append(ReadOnlySpan)` / `TextWriter.Write(ReadOnlySpan)` | `#if NETSTANDARD2_1_OR_GREATER || NET` | converters write span; skip temp string |
| `sprintf` string sink | `printf.fs:1029–1071` | pooled `char[]` (`ArrayPool`) + `String.Create` | `#if NETSTANDARD2_1_OR_GREATER || NET` | collapse N intermediate strings → 1 |
## 7 · Generic math *(net-only)*
| F# affected | FSharp.Core file | .NET API | guard | win |
|---|---|---|---|---|
| `GenericZero/OneDynamicImplTable` (`Half`) | `prim-types.fs:2667/2700` | `typeof` fast case (net5) | `#if NET5_0_OR_GREATER` | avoid reflection |
## No boost (documented negatives)
- `GenericEquality{Single,Double}Array` (`prim-types.fs:1511/1527`) — NaN-reflexivity (`er` flag: `NaN = NaN`) diverges from IEEE `SequenceEqual`; keep scalar.
- `DefaultInterpolatedStringHandler` as the printf **sink** — it is a `ref struct`, cannot be a field of the heap `PrintfEnv` class; and `sprintf`'s `ToStringAndClear()` is itself just `String.Create` over a pool. DISH belongs to a compiler lowering.
Contributor guide
Research direction
Start by selecting one narrowly scoped row, then read its named entry point in array.fs, seq.fs, prim-types.fs, string.fs, or printf.fs and check the stated target API and guard. Compare the documented semantic constraints, especially numeric overflow and NaN behavior. Done means the selected fast path is guarded as specified, preserves the stated semantics and non-target assets, and delivers the listed allocation or performance benefit.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- fsharp
- Domain
- performance
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 30/100