Skip to content

IL: hold custom attributes in fields rather than a union case - #20287

Open
auduchinok wants to merge 2 commits into
dotnet:mainfrom
auduchinok:il-ilAttributesStored
Open

IL: hold custom attributes in fields rather than a union case#20287
auduchinok wants to merge 2 commits into
dotnet:mainfrom
auduchinok:il-ilAttributesStored

Conversation

@auduchinok

@auduchinok auduchinok commented Aug 18, 2026

Copy link
Copy Markdown
Member

ILAttributesStored wrapped its state in a union:

type internal ILAttributesStoredRepr =
    | Reader of (int32 -> ILAttribute[])
    | Given of ILAttributes

so every attribute owner — type def, method, field, property, event, parameter, interface impl —
allocated a wrapper on top of the stored object itself. In a held FSharp.Common analysis 67,605 of 86,667
wrappers are Reader: 78% of owners never have their attributes read, and the wrapper existed only to
record that.

The change

type ILAttributesStored private (metadataIndex: int32, reader: int32 -> ILAttribute[], given: ILAttribute[] | null)

The reader function is shared per metadata reader per attribute table, so it can be held directly;
ILAttributes is a struct over the array, so rewrapping on each read allocates nothing. CreateGiven
stores the array with a shared noReader sentinel to keep the field non-null. Net per owner: −24 bytes of
wrapper, +8 for the second field. [<VolatileField>] is kept, and the race is the same benign one as
before — two threads may both run the reader and store equal results.

No API change: ILAttributesStoredRepr was internal and not in il.fsi; the members keep their
signatures and the union type is deleted.

Measured

Against 90767c77e9 built in the same session. er-base2 is a byte-identical copy of that baseline run
through the whole harness, so its column is the measurement error; a delta not exceeding both it and the
baseline's own spread is marked not established.

One project held open (mean of 3 fresh processes)

Project Before After Retained
consoleapp 33.84 MB 33.57 MB -0.27 MB (-0.79%)
Oxpecker 72.83 MB 72.27 MB -0.56 MB (-0.77%)
IcedTasks 50.14 MB 49.94 MB -0.20 MB (-0.39%)
FsToolkit 75.39 MB 75.20 MB -0.19 MB (-0.25%)
Fantomas.Benchmarks 64.77 MB 64.57 MB -0.20 MB (-0.31%)
Prime 108.37 MB 108.07 MB -0.30 MB (-0.28%)
Fantomas.Core 109.50 MB 109.27 MB -0.24 MB (not established)
Fantomas.Core.Tests 141.95 MB 141.65 MB -0.31 MB (-0.22%)
FSharp.Common 297.67 MB 296.35 MB -1.32 MB (-0.45%)
fcs 1273.30 MB 1271.81 MB -1.48 MB (not established)

A whole solution held open at once (mean of 2 passes)

Solution Before After Retained
resharper 1746.27 MB 1743.97 MB -2.29 MB (-0.13%)
fcsrepo 2557.91 MB 2554.06 MB -3.85 MB (-0.15%)
fantomas 690.18 MB 689.26 MB -0.92 MB (-0.13%)
oxpecker 254.06 MB 253.00 MB -1.06 MB (-0.42%)
fstoolkit 238.00 MB 237.56 MB -0.44 MB (not established)
icedtasks 212.48 MB 211.87 MB -0.61 MB (-0.28%)
prime 231.86 MB 231.39 MB -0.47 MB (-0.20%)
consoleapp 33.82 MB 33.62 MB -0.20 MB (-0.60%)

What it removes

gcdump of one held FSharp.Common analysis: Reader 67,605 → 1,467, Given 19,062 → 0 — 1.99 MB of
wrappers — against ILAttributesStored growing 2.60 → 3.25 MB for the second field. Net 1.34 MB predicted,
1.32 MB measured. The 1,467 remaining Reader objects are ILSecurityDeclsStored's own union, untouched
here.

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) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

❗ Release notes required

You can open this PR in browser to add release notes: open in github.dev


✅ Found changes and release notes in following paths:

Warning

No PR link found in some release notes, please consider adding it.

Change path Release notes path Description
`src/Compiler` docs/release-notes/.FSharp.Compiler.Service/11.0.100.md No current pull request URL (#20287) found, please consider adding it

@github-actions github-actions Bot added the AI-Tooling-Check-Scanned-Clean Tooling check: diff analyzed, no interesting infrastructure files label Aug 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

AI-Tooling-Check-Scanned-Clean Tooling check: diff analyzed, no interesting infrastructure files

Projects

Status: New

Development

Successfully merging this pull request may close these issues.

1 participant