Skip to content

Share the empty-array singleton for zero-length Array results - #20388

Merged
T-Gro merged 9 commits into
mainfrom
t-gro-array-collect-empty-perf
Sep 16, 2026
Merged

T-Gro merged 9 commits into
mainfrom
t-gro-array-collect-empty-perf

Conversation

@T-Gro

@T-Gro T-Gro commented Aug 27, 2026 •

Copy link
Copy Markdown
Member

Fixes #20382

Array.collect/Array.map and their kin allocated a fresh zero-length array for empty results — Array.collect on empty input allocated two (the intermediate 'U[][] and the concat output). Now the internal allocation primitive zeroCreateUnchecked hands back the shared System.Array.Empty<_>() singleton when the length is 0, so empty results across Array and Array.Parallel — and even zeroCreate/create/init at length 0 — allocate nothing.

Empty-sharing is the default: the handful of sites where the length is provably > 0 opt out via zeroCreateUncheckedNonEmpty to skip the branch. Non-empty results are unaffected.

@github-actions

github-actions Bot commented Aug 27, 2026 •

Copy link
Copy Markdown
Contributor

⚠️ Release notes required, but author opted out

Warning

Author opted out of release notes, check is disabled for this pull request.
cc @dotnet/fsharp-team-msft

@github-actions github-actions Bot added the AI-Tooling-Check-Bypassed Tooling check: non-fork PR, not diff-analyzed label Aug 27, 2026
@T-Gro
T-Gro marked this pull request as draft August 27, 2026 22:00
@T-Gro
T-Gro force-pushed the t-gro-array-collect-empty-perf branch from 5ce67ee to a0cb3f5 Compare August 27, 2026 23:16
@Lanayx

Lanayx commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Shouldn't the same optimization be done for empty list as well?

@Numpsy

Numpsy commented Aug 28, 2026

Copy link
Copy Markdown

I think there's a few other places that could possibly do the same. e.g. Seq.toArray appears to be already special casing an empty array in the general fallback path, but not in the ICollection special case

@T-Gro
T-Gro force-pushed the t-gro-array-collect-empty-perf branch from 677538e to 12c7027 Compare September 1, 2026 08:21
@T-Gro

T-Gro commented Sep 1, 2026

Copy link
Copy Markdown
Member Author

@Lanayx : List already flows trough the shared singleton.
@Numpsy : Let's tackle this as well by changing the central helpers for array creation.

@T-Gro
T-Gro force-pushed the t-gro-array-collect-empty-perf branch 3 times, most recently from 9290046 to 3f5f8ce Compare September 1, 2026 12:25
Array.collect/Array.map and many sibling builders allocated a fresh zero-length
array for empty results (Array.collect on empty input allocated two). Make the
internal allocation primitives Basics.Array.zeroCreateUnchecked and
Basics.List.arrayZeroCreate return the shared System.Array.Empty<_>() singleton
(via the [||] literal) when count = 0, so empty results across Array,
Array.Parallel and the toArray conversions (List.toArray, Array.ofList/ofSeq,
Seq.toArray, Set/Map.toArray) allocate nothing. Array.copy and Seq.toArray no
longer clone an empty source array, and Array.insertManyAt reuses Array.copy.

The count = 0 check is a single well-predicted branch; provably-non-empty callers
pay only that. Empty F# lists are already the interned FSharpList.Empty singleton,
so there is no list-result change. The empty-guard inlines into FSharp.Core's
methods, growing the trimmed FSharp.Core.dll by 512 bytes; trimming baselines
updated accordingly.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@T-Gro
T-Gro force-pushed the t-gro-array-collect-empty-perf branch from 3f5f8ce to aadb137 Compare September 1, 2026 12:58
@T-Gro
T-Gro marked this pull request as ready for review September 1, 2026 14:42
@T-Gro T-Gro added the NO_RELEASE_NOTES Label for pull requests which signals, that user opted-out of providing release notes label Sep 1, 2026
@T-Gro
T-Gro requested a review from abonie September 1, 2026 14:42
@T-Gro
T-Gro enabled auto-merge (squash) September 1, 2026 14:42

@T-Gro T-Gro left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 🕵️ AI review — verify independently.

Comment thread src/FSharp.Core/array.fs
@T-Gro T-Gro added the AI-reviewed PR reviewed by AI review council label Sep 9, 2026
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@T-Gro

T-Gro commented Sep 10, 2026

Copy link
Copy Markdown
Member Author

All done.

Comment thread src/FSharp.Core/array.fs
@github-project-automation github-project-automation Bot moved this from New to In Progress in F# Compiler and Tooling Sep 11, 2026
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@T-Gro

T-Gro commented Sep 16, 2026

Copy link
Copy Markdown
Member Author

@copilot resolve the merge conflicts in this pull request

…mpty-perf

# Conflicts:
#	tests/AheadOfTime/Trimming/check.ps1

Co-authored-by: T-Gro <46543583+T-Gro@users.noreply.github.com>
auto-merge was automatically disabled September 16, 2026 09:30

Head branch was pushed to by a user without write access

Copilot AI commented Sep 16, 2026

Copy link
Copy Markdown
Contributor

@copilot resolve the merge conflicts in this pull request

Merged origin/main in 07f5988. The only conflict was tests/AheadOfTime/Trimming/check.ps1: main lowered the net9.0 baselines by 1024 bytes while this branch raised them by 512, so the merged expectations are the combined deltas (FSharp.Core.dll 310784, StaticLinkedFSharpCore_Trimming_Test.dll 9174016). src/FSharp.Core/array.fs/.fsi auto-merged with main's new inline iter2/iteri/forall2 etc., and FSharp.Core builds clean in Release.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@T-Gro
T-Gro merged commit 1636ef4 into main Sep 16, 2026
52 checks passed
@github-project-automation github-project-automation Bot moved this from In Progress to Done in F# Compiler and Tooling Sep 16, 2026
@T-Gro
T-Gro deleted the t-gro-array-collect-empty-perf branch September 16, 2026 19:20
bartelink pushed a commit to bartelink/fsharp that referenced this pull request Oct 7, 2026
…#20388)

* Share the empty-array singleton for zero-length results (dotnet#20382)

Array.collect/Array.map and many sibling builders allocated a fresh zero-length
array for empty results (Array.collect on empty input allocated two). Make the
internal allocation primitives Basics.Array.zeroCreateUnchecked and
Basics.List.arrayZeroCreate return the shared System.Array.Empty<_>() singleton
(via the [||] literal) when count = 0, so empty results across Array,
Array.Parallel and the toArray conversions (List.toArray, Array.ofList/ofSeq,
Seq.toArray, Set/Map.toArray) allocate nothing. Array.copy and Seq.toArray no
longer clone an empty source array, and Array.insertManyAt reuses Array.copy.

The count = 0 check is a single well-predicted branch; provably-non-empty callers
pay only that. Empty F# lists are already the interned FSharpList.Empty singleton,
so there is no list-result change. The empty-guard inlines into FSharp.Core's
methods, growing the trimmed FSharp.Core.dll by 512 bytes; trimming baselines
updated accordingly.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Address review feedback

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Clarify empty-array sharing in copy and insertManyAt docs

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Fix CI failures

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: perf-bundle <perf@local>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

AI-reviewed PR reviewed by AI review council AI-Tooling-Check-Bypassed Tooling check: non-fork PR, not diff-analyzed NO_RELEASE_NOTES Label for pull requests which signals, that user opted-out of providing release notes

Projects

Archived in project

Development

Successfully merging this pull request may close these issues.

Performance of Array.collect with empty inputs

5 participants