Skip to content

Fix ValueLazy caching being defeated by readonly fields#706

Merged
tannergooding merged 1 commit into
dotnet:mainfrom
tannergooding:fix-readonly-valuelazy-caching
Jul 12, 2026
Merged

Fix ValueLazy caching being defeated by readonly fields#706
tannergooding merged 1 commit into
dotnet:mainfrom
tannergooding:fix-readonly-valuelazy-caching

Conversation

@tannergooding

Copy link
Copy Markdown
Member

ValueLazy<T> is a mutable struct: its Value getter computes the value on first access and clears the factory so subsequent accesses return the cached value. Every field storing one was declared readonly, so each access operated on a defensive copy and the cached result was discarded -- the factory ran again on every access, defeating the memoization the type exists to provide. This is the classic mutable-struct-in-a-readonly-field bug.

Dropping readonly from all 326 ValueLazy<T> fields (170 files) lets the cached value persist. The factories are idempotent, so behavior is unchanged; access to lazy members (spellings, parents, translation units, etc.) simply no longer recomputes each time -- which avoids repeated native clang_* calls, GetOrCreate lookups, and string marshaling.


Measured on a representative real-world workload -- the d3d12 generation from terrafx/terrafx.interop.windows against the Windows SDK:

  • Wall-clock: 4404 -> 4230 ms median (~4-5%), with clean separation (baseline min 4342 > fixed max 4241).
  • Allocations: 284.9 -> 280.2 MB (~1.6%) under full optimization.

I audited the rest of the runtime, generator, and interop layers for the same pattern: the descriptor structs (StructDesc, ValueDesc, FunctionOrDelegateDesc, etc.) are mutable but only ever used as locals or by-value parameters, and LazyList is a sealed class -- ValueLazy was the only type caught in this trap.

All 3706 tests pass with a 0-warning build.

ValueLazy<T> is a mutable struct: its Value getter computes the value on
first access and clears the factory so subsequent accesses return the
cached value. Every field storing one was declared readonly, so each
access operated on a defensive copy and the cached result was discarded
-- the factory ran again on every access, defeating the memoization the
type exists to provide.

Drop readonly from all ValueLazy<T> fields so the cached value persists.
The factories are idempotent, so behavior is unchanged; access to lazy
members (spellings, parents, translation units, etc.) no longer
recomputes each time.

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