From 06ad8f983de703ee79be65064903381a80e0149d Mon Sep 17 00:00:00 2001 From: Eugene Auduchinok Date: Fri, 14 Aug 2026 13:08:10 +0200 Subject: [PATCH 1/2] IL: hold custom attributes in fields rather than a union case ILAttributesStored wrapped its state in an ILAttributesStoredRepr union, so every attribute owner allocated a wrapper object on top of the stored object itself: 67,605 Reader wrappers on a 489-reference project, which is exactly the count of owners whose attributes are never read, plus a Given wrapper per owner that is. The reader function is shared per metadata reader per attribute table, so it can be held directly, and ILAttributes is a struct over the array, so rewrapping the array on each read allocates nothing. Holding the array and the reader in fields removes 24 bytes of wrapper per owner and adds 8 for the second field. Retained memory after ParseAndCheckProject drops 0.19-1.30 MB per project (-0.05% to -0.84%) across the measurement suite, and the union type is deleted. ILAttributesStoredRepr was not in the signature file, so there is no API change. Co-Authored-By: Claude Opus 5 (1M context) --- .../.FSharp.Compiler.Service/11.0.100.md | 1 + src/Compiler/AbstractIL/il.fs | 32 +++++++++++-------- 2 files changed, 19 insertions(+), 14 deletions(-) diff --git a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md index 7dd68964cb3..97589376dc8 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md +++ b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md @@ -166,6 +166,7 @@ * Add symbol and type highlighting to F# diagnostics ([PR #20097](https://github.com/dotnet/fsharp/pull/20097)) * IL: add `ILPreNamespace`, make `ILPreTypeDef` creation lazy ([PR #20092](https://github.com/dotnet/fsharp/pull/20092)) * IL: use empty tables for members when possible ([PR #20249](https://github.com/dotnet/fsharp/pull/20249)) +* IL: hold custom attributes in fields rather than a union case ### Improved diff --git a/src/Compiler/AbstractIL/il.fs b/src/Compiler/AbstractIL/il.fs index 0aa4e76ecf4..faac45cbbfe 100644 --- a/src/Compiler/AbstractIL/il.fs +++ b/src/Compiler/AbstractIL/il.fs @@ -1262,14 +1262,18 @@ type WellKnownILAttributes = | OverloadResolutionPriorityAttribute = (1u <<< 26) | NotComputed = (1u <<< 31) -type internal ILAttributesStoredRepr = - | Reader of (int32 -> ILAttribute[]) - | Given of ILAttributes - [] -type ILAttributesStored private (metadataIndex: int32, initial: ILAttributesStoredRepr) = +type ILAttributesStored private (metadataIndex: int32, reader: int32 -> ILAttribute[], given: ILAttribute[] | null) = + + /// Stands in for the reader when the attributes are already in hand, so the field can stay non-null. + static let noReader: int32 -> ILAttribute[] = fun _ -> [||] + + // Holds the array rather than an ILAttributesStoredRepr. The reader function is shared per metadata + // reader per attribute table, so it is held directly; wrapping it cost one object per owner, and most + // owners are never forced, so most of those existed only to say "not read yet". ILAttributes is a + // struct over the array, so rewrapping on each read allocates nothing. [] - let mutable repr = initial + let mutable attrArray: ILAttribute[] | null = given [] let mutable wellKnownFlags = WellKnownILAttributes.NotComputed @@ -1277,12 +1281,12 @@ type ILAttributesStored private (metadataIndex: int32, initial: ILAttributesStor member _.MetadataIndex = metadataIndex member x.CustomAttrs: ILAttributes = - match repr with - | Given a -> a - | Reader f -> - let r = ILAttributes(f metadataIndex) - repr <- Given r - r + match attrArray with + | null -> + let a = reader metadataIndex + attrArray <- a + ILAttributes a + | a -> ILAttributes a member x.HasWellKnownAttribute(flag: WellKnownILAttributes, compute: ILAttributes -> WellKnownILAttributes) : bool = x.GetOrComputeWellKnownFlags(compute) &&& flag <> WellKnownILAttributes.None @@ -1298,9 +1302,9 @@ type ILAttributesStored private (metadataIndex: int32, initial: ILAttributesStor wellKnownFlags <- computed computed - static member CreateReader(idx: int32, f: int32 -> ILAttribute[]) = ILAttributesStored(idx, Reader f) + static member CreateReader(idx: int32, f: int32 -> ILAttribute[]) = ILAttributesStored(idx, f, null) - static member CreateGiven(attrs: ILAttributes) = ILAttributesStored(-1, Given attrs) + static member CreateGiven(attrs: ILAttributes) = ILAttributesStored(-1, noReader, attrs.AsArray()) let emptyILCustomAttrs = ILAttributes [||] From e0a9274f3fbb12dd94d1d304ac399ff91ce4ae8e Mon Sep 17 00:00:00 2001 From: Eugene Auduchinok Date: Tue, 18 Aug 2026 15:27:23 +0200 Subject: [PATCH 2/2] Fantomas --- src/Compiler/AbstractIL/il.fs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Compiler/AbstractIL/il.fs b/src/Compiler/AbstractIL/il.fs index faac45cbbfe..4d33e1ff78b 100644 --- a/src/Compiler/AbstractIL/il.fs +++ b/src/Compiler/AbstractIL/il.fs @@ -1304,7 +1304,8 @@ type ILAttributesStored private (metadataIndex: int32, reader: int32 -> ILAttrib static member CreateReader(idx: int32, f: int32 -> ILAttribute[]) = ILAttributesStored(idx, f, null) - static member CreateGiven(attrs: ILAttributes) = ILAttributesStored(-1, noReader, attrs.AsArray()) + static member CreateGiven(attrs: ILAttributes) = + ILAttributesStored(-1, noReader, attrs.AsArray()) let emptyILCustomAttrs = ILAttributes [||]