Sort the merged property definitions in one array - #20456
Open
xperiandri wants to merge 3 commits into
Open
Conversation
Contributor
❗ Release notes requiredYou can open this PR in browser to add release notes: open in github.dev
|
Contributor
|
🔍 Tooling Safety Check — Affects-Compiler-Output
|
T-Gro
requested changes
Sep 7, 2026
T-Gro
left a comment
Member
There was a problem hiding this comment.
Pls check a few [MicroPerf] or PRs from Eugene on memory reduction - try to provide any sorts of numbers (eg. allocations within a particular method or of a particular type).
HashRangeSorted built three intermediate lists - one from the comprehension, one from List.sortBy, one from List.map - plus the sort's own array, and ran for every type definition the code generator emits. Read out of FSharp.Compiler.Service.dll and FSharp.Core.dll, 80% of type definitions carry no properties at all and 94% carry at most three, so the empty case is the one worth being cheap. It now returns immediately when there is nothing to sort, and otherwise fills one exact-size array through Seq.toArray's ICollection fast path and sorts it in place. List.sortBy is stable where Array.sortInPlaceBy is not, but the sort keys are the insertion indices AddPropertyDefToHash assigns via ht[nm] <- (ht.Count, pdef), which are never duplicated, so the ordering that reaches mkILProperties is unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
xperiandri
force-pushed
the
perf/ilxgen-list-creation
branch
from
September 9, 2026 20:12
560a0cd to
2f15788
Compare
IlxGen.fsThe two StackUnexpected entries came from the closures the List pipeline emitted where FSharpFunc was expected. Sorting the array in place no longer produces them. Verified by running dotnet ilverify over both Release targets of the built FSharp.Compiler.Service: ten errors each, none in HashRangeSorted, matching these baselines exactly. The Debug baselines never carried the entries. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
xperiandri
commented
Sep 9, 2026
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
HashRangeSortedbuilt three intermediate lists to handTypeDefBuilder.Closeits merged property definitions:one list from the comprehension, one from
List.sortBy, one fromList.map, plus the sort's own array — and it ran for every type definition the code generator emits, including the large majority that have no properties at all.It now returns immediately when there is nothing to sort, and otherwise fills one exact-size array and sorts it in place:
Array.ofSeqoverht.ValuestakesSeq.toArray'sICollectionfast path (Array.zeroCreateUnchecked+CopyTo), so there is no enumerator and no over-allocation.No behavioural change: the same property definitions reach
mkILPropertiesin the same order.List.sortByis stable andArray.sortInPlaceByis not, but the sort keys are the insertion indicesAddPropertyDefToHashassigns viaht[nm] <- (ht.Count, pdef)— never duplicated — so an unstable sort produces an identical ordering.Why this shape
The input is overwhelmingly small. Property counts per type definition, read out of
FSharp.Compiler.Service.dll(22,167 type defs) andFSharp.Core.dll(2,261) withSystem.Reflection.Metadata:p50 = 0, p90 = 2, p99 = 11. The
Dictionary<_, _>(3, HashIdentity.Structural)capacity hint inTypeDefBuilderwas right about the sizes involved, so the empty case is the one that has to be cheap.Benchmarks
BenchmarkDotNet, medium job, .NET 10,
MemoryDiagnoser, over a faithful stand-in forTypeDefBuilder.Close's property step (compiler internals are not public, so the two bodies are replicated verbatim against anIDictionaryof the same shape).mainFaster and no more allocating at every size, and roughly 3× faster on the empty case that accounts for 80% of calls.
Benchmark source
Two approaches this PR previously took, and dropped
Earlier revisions moved
HashRangeSortedontoSeqand replaced the@concatenations inTypeDefBuilder.Close/GenTypeDefwith[ yield! …; yield! … ]. Measurement rejected both.Seqinstead ofList. Aseq { } |> Seq.sortBy |> Seq.mappipeline costs a fixed ~336 B in wrappers and enumerators that theListpipeline does not pay when the dictionary is empty. Against the distribution above it was 9.3× slower and +336 B at 0 properties, 4.5× the allocation at 1, and only broke even past ~12 — beyond p99. Rough arithmetic over FCS's own type definitions put it at roughly +7 MB of extra allocation to compile FCS.[ yield! a; yield! b ]instead ofa @ b. The premise — that@"forces both sides to lists" — does not hold when both operands already are lists, which they are here.(@)inprim-types.fsreturns the other operand untouched when either side is empty and otherwise copies only the left:[ yield! a; yield! b ]copiesaand shares only the finalyield!, so it ties@when the tail is non-empty (and is ~25% faster there) but allocates|a|cons cells where@allocated none. At the median shape —methodDefs @ augmentOverrideMethodDefs @ abstractMethodDefs, where the latter two are empty for ordinary types — it went 0 B → 64 B and 4.2× slower. Those concatenations are therefore left exactly as they were.ILVerify baselines
The Release baselines carried two
HashRangeSortedStackUnexpectedentries, from the closures theListpipeline emitted whereFSharpFuncwas expected. Sorting the array in place no longer produces them, so both files lose those two lines.Verified by running
dotnet ilverify --sanity-checks --tokensover both Release targets of the builtFSharp.Compiler.Service(netstandard2.0and the netcoreapp TFM): ten errors each, none inHashRangeSorted, matching the updated baselines exactly. The Debug baselines never carried the entries and are unchanged.Checklist
docs/release-notes/.FSharp.Compiler.Service/11.0.100.md🤖 Generated with Claude Code