Skip to content

Avoid allocating per-instance lazy factory delegates using function pointers - #707

Merged
tannergooding merged 1 commit into
dotnet:mainfrom
tannergooding:fnptr-valuelazy
Jul 12, 2026
Merged

Avoid allocating per-instance lazy factory delegates using function pointers#707
tannergooding merged 1 commit into
dotnet:mainfrom
tannergooding:fnptr-valuelazy

Conversation

@tannergooding

@tannergooding tannergooding commented Jul 12, 2026

Copy link
Copy Markdown
Member

Every lazily-computed Cursor/Decl/Type member is wrapped in a ValueLazy<T> whose factory is a captured Func<T>. Because the factory closes over the owning instance (and, in a number of cases, additional constructor state), each wrapper eagerly allocates a delegate -- and sometimes a display class -- in the constructor, none of which is needed once the value has been computed.

This replaces ValueLazy<T> with ValueLazy<TSelf, T>, whose factory is a static delegate*<TSelf, T> passed the owning instance explicitly. The function pointer is a static, so nothing is allocated per instance; the owning instance is threaded through GetValue(self) at the call site. Each captured lambda becomes a static factory method taking self.


Measured on the TerraFX Windows SDK generation (DirectX/d3d12) with DOTNET_TieredCompilation=0 (the NativeAOT-representative full-opt proxy the shipped tool runs under), sampled GCAllocationTick:

Before After
Total sampled allocations 280.3 MB 179.6 MB
Per-instance Func<T> factory delegates ~71 MB (the top allocators) 0

Every Func_1[...] (String, Cursor, Decl, TranslationUnit, Type, IDeclContext, ...) is gone. The remaining Func_2/Func_3 allocations belong to LazyList, which is a separate sealed class and out of scope here.

All 3706 generator tests pass and the build is warning-clean. -- The bulk of the diff is mechanical (326 fields, 327 factories across 172 files); it was produced with a Roslyn rewriter driven by the semantic model, so cases like the static TranslationUnit.GetOrCreate vs. the instance TranslationUnit property are disambiguated correctly rather than by text matching.

Note

This PR body was drafted by Copilot.

…ointers

The object model wraps every lazily-computed Cursor/Decl/Type member in a
ValueLazy<T> whose factory is a captured Func<T>. Because the factory closes
over the owning instance, each wrapper allocates a delegate (and, for factories
that capture additional state, a display class) up front in the constructor --
roughly 65 MB of Func<T> delegates when generating the Windows SDK, none of
which are needed once the value is computed.

Replace ValueLazy<T> with ValueLazy<TSelf, T>, whose factory is a static
delegate*<TSelf, T> passed the owning instance explicitly. The function pointer
is a static, so no per-instance delegate is allocated; the owning instance is
threaded through GetValue(self) at the call site. Each captured lambda becomes a
static factory method taking self.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@tannergooding
tannergooding merged commit b43c6cd into dotnet:main Jul 12, 2026
14 checks passed
@tannergooding
tannergooding deleted the fnptr-valuelazy branch July 12, 2026 21:55
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