Skip to content

Avoid allocating per-cursor LazyList factory delegates using function pointers#715

Merged
tannergooding merged 1 commit into
dotnet:mainfrom
tannergooding:perf-lazylist-fnptr
Jul 13, 2026
Merged

Avoid allocating per-cursor LazyList factory delegates using function pointers#715
tannergooding merged 1 commit into
dotnet:mainfrom
tannergooding:perf-lazylist-fnptr

Conversation

@tannergooding

Copy link
Copy Markdown
Member

LazyList<T> stored a Func<int, T> (or Func<int, T?, T>) factory per instance, which allocated a closure delegate for every cursor that exposed a lazy child list -- even when the list was empty (the common case for Decl._attrs). Mirroring the ValueLazy<TSelf, T> conversion in #707, the factory is now a delegate*<object, int, T> function pointer plus a stored object _self, so the per-instance closure allocation is gone.

All owners (Cursor, Type, TemplateArgument) are reference types, so passing this as object self never boxes. Each former lambda becomes a static factory method that casts self back to the owning type. The four nested _templateParameterLists sites are deduplicated into a shared Decl.CreateTemplateParameterLists helper.


Measured on the TerraFX d3d12 generation workload (gc-verbose, DOTNET_TieredCompilation=0): Func<int, Decl, Decl> (~2.0 MB) and Func<int, Attr> (~1.9 MB) are eliminated entirely, dropping sampled allocations from ~142.6 MB to ~134.2 MB. No behavioral change; all 3708 golden tests pass, output is byte-identical.

Note

This PR description was drafted by Copilot.

… pointers

`LazyList<T>` previously stored a `Func<int, T>` (or `Func<int, T?, T>`)
factory per instance, which allocated a closure delegate for every cursor
that exposed a lazy child list -- even when the list was empty (the common
case for `Decl._attrs`). Mirroring the `ValueLazy<TSelf, T>` conversion,
the factory is now a `delegate*<object, int, T>` function pointer plus a
stored `object _self`, so the closure allocation is gone.

All owners (`Cursor`, `Type`, `TemplateArgument`) are reference types, so
passing `this` as `object self` never boxes. Each former lambda becomes a
`static` factory method that casts `self` back to the owning type.

Measured on the TerraFX d3d12 generation workload (gc-verbose,
`DOTNET_TieredCompilation=0`): `Func<int, Decl, Decl>` (~2.0 MB) and
`Func<int, Attr>` (~1.9 MB) are eliminated entirely, dropping sampled
allocations from ~142.6 MB to ~134.2 MB. No behavioral change; all 3708
golden tests pass.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@tannergooding
tannergooding merged commit e24c766 into dotnet:main Jul 13, 2026
14 checks passed
@tannergooding
tannergooding deleted the perf-lazylist-fnptr branch July 13, 2026 02:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant