Skip to content

Make MethodDataObject::FillEntryDataForAncestor lazy per-level - #131176

Merged
davidwrighton merged 1 commit into
dotnet:mainfrom
davidwrighton:fillentry-lazy-perlevel
Jul 22, 2026
Merged

Make MethodDataObject::FillEntryDataForAncestor lazy per-level#131176
davidwrighton merged 1 commit into
dotnet:mainfrom
davidwrighton:fillentry-lazy-perlevel

Conversation

@davidwrighton

Copy link
Copy Markdown
Member

Summary

Follow-up to #130151. That PR optimized MethodTable::MethodDataObject::FillEntryDataForAncestor to pre-resolve virtual slots whose final vtable entry is NULL — slots that have never been overridden by a subclass nor called, and therefore cannot be the target of a MethodImpl, so they are safe to fill even when a MethodImpl exists somewhere in the inheritance chain. This avoids the expensive GetMethodDescForSlot_NoThrow fallback (O(N) in the number of methods in the type hierarchy) that otherwise makes MethodDataObject population O(V*N) for deep hierarchies with many un-overridden/uncalled virtuals — the shape that shows up in WPF startup (dotnet/wpf#11740).

However, that implementation produced the null-slot bitmap on the initial MethodTable (m_pDeclMT) and, to fill those slots across the hierarchy, added an eager loop that walked the entire parent chain (running an IntroducedMethodIterator at every level) on the very first PopulateNextLevel call. That defeats the class's lazy, one-level-per-PopulateNextLevel design and adds tail latency.

Change

MethodDataObject already retains a pointer to the initial MethodTable, so the "is this slot's final vtable entry NULL" question can be answered on demand from m_pDeclMT's canonical MethodTable at any level. This PR computes the set of un-overridden/uncalled slots per level instead:

  • For each virtual slot introduced at the current level, when a MethodImpl exists at or below the current level, fill it only if its final vtable entry on m_pDeclMT's canonical MT is NULL.
  • m_containsMethodImpl is monotonic across the most-derived → base walk, so at each level it reflects exactly the MethodImpls that can redirect slots introduced at that level (a MethodImpl at a more-base level targets lower slot numbers and cannot affect slots introduced further derived).

This removes the eager full-chain walk, the null-slot bitvector, and its heap allocation, restoring a single IntroducedMethodIterator per level while producing identical Decl/Impl MethodDesc data. Null slots are introduced at exactly one level (never overridden), so they are filled exactly once, at their introducing level.

Net: -167 / +36 lines; the method is now allocation-free under its LIMITED_METHOD_CONTRACT.

Validation

  • Builds clean (CoreCLR Debug and Release, Windows x64).

  • Ran a .NET 10 self-contained WPF app on a locally-built .NET 11 runtime (only the app's own binaries stay .NET 10; the full .NET 11 Microsoft.NETCore.App and .NET 11 WPF pack are used). WPF heavily exercises this exact path; the app renders correctly with no errors.

  • Startup A/B vs current main (swapping only the matched clrjit/coreclr/System.Private.CoreLib trio), interleaved, medians of 60 iterations:

    Config Median (ms) Mean (ms) Min Max
    main (baseline) 1407.0 1435.1 1356.8 3190.8
    this PR 1395.7 1395.1 1359.0 1430.9

    Performance-neutral-to-slightly-faster at the median, with a much tighter tail (max 1431 ms vs 3191 ms) from removing the eager full-chain walk. (Absolute numbers are inflated because the framework here is not R2R/PGO-optimized for a dev coreclr; the comparison isolates this change.)

Note

This pull request was authored with assistance from GitHub Copilot.

PR dotnet#130151 optimized FillEntryDataForAncestor to pre-resolve virtual slots
whose final vtable entry is NULL (never overridden or called), which cannot be
MethodImpl targets, so they are safe to fill even when a MethodImpl exists in
the hierarchy. However, producing the null-slot bitmap was done on the initial
MethodTable (m_pDeclMT), and to fill those slots across the hierarchy it added
an eager loop that walked the entire parent chain on the first PopulateNextLevel
call, defeating the class's lazy per-level design and adding tail latency.

Since MethodDataObject retains a pointer to the initial MethodTable, the null
slots for the entries introduced at any level can be computed on demand from
m_pDeclMT's canonical MethodTable. Compute unprocessedNonOverriddenSlots per
level instead: for each introduced virtual slot, when a MethodImpl exists at or
below the current level (m_containsMethodImpl is monotonic across the upward
walk), fill it only if its final vtable entry is NULL. This removes the eager
full-chain walk, the null-slot bitvector, and its heap allocation, and restores
one IntroducedMethodIterator per level while producing identical Decl/Impl data.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d6eeba00-418e-42e7-8e32-762bc1cedeb6
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 3 pipeline(s).
13 pipeline(s) were filtered out due to trigger conditions.
There may be pipelines that require an authorized user to comment /azp run to run.

@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @agocke
See info in area-owners.md if you want to be subscribed.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR refactors MethodTable::MethodDataObject::FillEntryDataForAncestor to preserve MethodDataObject’s intended lazy, one-ancestor-per-PopulateNextLevel() population behavior when MethodImpls are present in the inheritance chain, while retaining the earlier optimization that pre-resolves “final vtable slot is NULL” virtuals to avoid O(V*N) fallback costs.

Changes:

  • Removes the eager full-parent-chain walk and the null-slot bitvector (and its potential heap allocation) previously done during the first PopulateNextLevel() call.
  • When m_containsMethodImpl is true, performs the “NULL final slot” check on-demand per level using m_pDeclMT’s canonical vtable to decide whether a virtual slot can be safely pre-resolved.
  • Keeps non-virtual slot pre-resolution restricted to the most-derived m_pDeclMT level, as before.

@davidwrighton
davidwrighton merged commit 026096b into dotnet:main Jul 22, 2026
112 checks passed
@dotnet-milestone-bot dotnet-milestone-bot Bot added this to the 11.0-rc1 milestone Jul 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants