Skip to content

Eliminate forwarder stubs by reusing method precodes for call counting indirection#124664

Open
davidwrighton wants to merge 4 commits intodotnet:mainfrom
davidwrighton:use_temporary_entrypoints_for_all_callcounting
Open

Eliminate forwarder stubs by reusing method precodes for call counting indirection#124664
davidwrighton wants to merge 4 commits intodotnet:mainfrom
davidwrighton:use_temporary_entrypoints_for_all_callcounting

Conversation

@davidwrighton
Copy link
Member

@davidwrighton davidwrighton commented Feb 20, 2026

For methods versioned with vtable slot backpatching (virtual/interface methods), the call counting infrastructure previously allocated separate forwarder stub precodes to serve as stable indirection points between vtable slots and deletable call counting stubs. This added memory overhead and complexity for tracking, resetting, and trimming these per-method forwarder stubs.

This change reuses the method's own temporary entry point (precode) as the stable indirection during call counting. Instead of allocating a forwarder stub and backpatching vtable slots to it, vtable slots are reset to point to the method's precode, and the precode target is redirected to the call counting stub via \SetTargetInterlocked. This preserves the safety property that vtable slots never point directly to deletable call counting stubs, while eliminating the separate forwarder stub allocation entirely.

Key invariant

For backpatchable methods, the temporary entry point's precode target must only ever be one of two values:

  1. The prestub (default, and whenever call counting is not active)
  2. A call counting stub (during active call counting only)

It must never point directly to native code. Setting the precode to native code would bypass \DoBackpatch()\ slot recording, preventing new vtable slots from being discovered. This would leave those slots permanently stale if a code version change (e.g. ReJIT) occurs later.

To maintain correct \DoBackpatch()\ behavior during call counting, \GetMethodEntryPoint()\ is kept at the native code entry point (not the temporary entry point). This prevents the \pExpected == pTarget\ early-exit check in \DoBackpatch()\ from short-circuiting slot recording once the precode reverts to prestub.

Components updated

  • CallCountingManager::SetCodeEntryPoint: For backpatchable methods, replaced forwarder stub creation with \BackpatchToResetEntryPointSlots()\ (to reset vtable slots to the precode) followed by \SetMethodEntryPoint(nativeCode)\ and \SetTargetInterlocked(callCountingStub)\ on the method's precode. When call counting completes (threshold reached or stage >= PendingCompletion), the precode is reset to prestub via \ResetTargetInterlocked()\ while \SetCodeEntryPoint()\ handles recorded slot updates through the normal \BackpatchEntryPointSlots()\ path.

  • CallCountingManager::CompleteCallCounting: Calls \SetCodeEntryPoint()/\ResetCodeEntryPoint()\ for all methods (backpatchable and non-backpatchable alike), then resets the precode to prestub via \ResetTargetInterlocked()\ for backpatchable methods. This ensures recorded slots are properly updated while the precode reverts to prestub for new slot discovery.

  • CallCountingManager::StopAllCallCounting: Calls \ResetCodeEntryPoint()\ for all methods, then resets the precode target to prestub via \ResetTargetInterlocked()\ for backpatchable methods. Removed the forwarder stub reset loop.

  • CallCountingManager::DeleteAllCallCountingStubs: Adds a safety-net \ResetTargetInterlocked()\ for backpatchable methods in Stage::Complete before deleting the call counting info. This guarantees the precode is at prestub even if an earlier reset was missed. Removed forwarder stub lookup and removal.

  • CallCountingManager::TrimCollections: Removed forwarder stub hash table trimming.

  • callcounting.h: Removed \MethodDescForwarderStubHashTraits, \MethodDescForwarderStubHash, and \m_methodDescForwarderStubHash. Updated documentation to describe the precode-reuse approach and the two-state precode invariant.

  • prestub.cpp: Updated \DoBackpatch()\ comments to document the precode invariant: the precode for backpatchable methods should only ever point to prestub or a call counting stub, never directly to native code.

…g indirection

For methods versioned with vtable slot backpatching (virtual/interface
methods), the call counting infrastructure previously allocated separate
forwarder stub precodes to serve as stable indirection points between
vtable slots and deletable call counting stubs. This added memory
overhead and complexity for tracking, resetting, and trimming these
per-method forwarder stubs.

This change reuses the method's own temporary entry point (precode) as
the stable indirection during call counting. Instead of allocating a
forwarder stub and backpatching vtable slots to it, vtable slots are
reset to point to the method's precode, and the precode target is
redirected to the call counting stub via SetTargetInterlocked. This
preserves the safety property that vtable slots never point directly to
deletable call counting stubs, while eliminating the separate forwarder
stub allocation entirely.

Components updated:

- CallCountingManager::SetCodeEntryPoint: For backpatchable methods,
  replaced forwarder stub creation with ResetCodeEntryPoint (to ensure
  vtable slots point to the precode) followed by SetTargetInterlocked
  on the method's precode. Non-final-tier entry point transitions
  (threshold reached, pending completion) also update the precode
  target instead of backpatching vtable slots.

- CallCountingManager::CompleteCallCounting: For backpatchable methods,
  updates the precode target to native code or resets it to prestub,
  rather than calling SetCodeEntryPoint/ResetCodeEntryPoint which would
  backpatch vtable slots.

- CallCountingManager::StopAllCallCounting: For backpatchable methods,
  resets the precode target via ResetTargetInterlocked instead of
  calling ResetCodeEntryPoint. Removed the forwarder stub reset loop.

- CallCountingManager::DeleteAllCallCountingStubs: Removed forwarder
  stub lookup and removal from the per-method cleanup loop.

- CallCountingManager::TrimCollections: Removed forwarder stub hash
  table trimming.

- callcounting.h: Removed MethodDescForwarderStubHashTraits,
  MethodDescForwarderStubHash, and the m_methodDescForwarderStubHash
  member. Updated header documentation to reflect the new design.
@dotnet-policy-service
Copy link
Contributor

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

Copy link
Contributor

Copilot AI left a comment

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 aims to reduce memory overhead and simplify tiered call counting for methods that use vtable slot backpatching by eliminating per-method forwarder precode stubs and instead attempting to reuse the method’s existing temporary entry point (precode) as the stable indirection point.

Changes:

  • Removed the forwarder-stub tracking hash table and associated traits/types from the call counting manager.
  • Updated call counting setup/completion/stop paths to redirect the method’s temporary entry point (precode) to call counting stubs or native code, instead of allocating/using a separate forwarder precode.
  • Simplified cleanup/trimming logic by removing forwarder-stub reset/removal and hash trimming.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

File Description
src/coreclr/vm/callcounting.h Updates design documentation and removes forwarder-stub hash member/types.
src/coreclr/vm/callcounting.cpp Removes forwarder-stub hash implementation and reworks call counting entrypoint manipulation to retarget the temporary entry point precode.

When redirecting the method's temporary entry point (precode) to a call
counting stub, the previous code called ResetCodeEntryPoint() which set
GetMethodEntryPoint() to the temporary entry point value. This broke
DoBackpatch() slot recording because DoBackpatch() treats
GetMethodEntryPoint() == GetTemporaryEntryPoint() as 'method not yet
published' and skips slot discovery and recording.

Replace ResetCodeEntryPoint() with BackpatchToResetEntryPointSlots()
followed by SetMethodEntryPoint(codeEntryPoint). This resets vtable slots
to the temporary entry point (so calls flow through the precode) while
keeping GetMethodEntryPoint() at the native code entry point. After call
counting stubs are deleted and the precode reverts to prestub, DoBackpatch()
will correctly record new vtable slots because GetMethodEntryPoint() differs
from GetTemporaryEntryPoint().

Updated documentation in prestub.cpp, callcounting.cpp, and callcounting.h
to describe the precode redirect lifecycle and the bounded window during
call counting where new vtable slots may not be recorded.
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

For backpatchable methods (virtual/interface with vtable slot backpatching),
the temporary entry point's precode target must only ever be:
  1. The prestub (default, and when call counting is not active)
  2. A call counting stub (during active call counting only)

Previously, several code paths set the precode target to native code when
call counting ended (CompleteCallCounting, SetCodeEntryPoint with completed
stages, call count threshold hit). This bypassed DoBackpatch() slot
recording: new vtable slots would jump directly to native code without
being discovered and recorded, so future code version changes (e.g. ReJIT)
could not update those slots.

Change all precode updates that previously used SetTargetInterlocked(native)
to use ResetTargetInterlocked() (which resets to prestub). This ensures:
- SetCodeEntryPoint() handles recorded slots via BackpatchEntryPointSlots()
- The precode reverts to prestub so new slots flow through DoBackpatch()
- DeleteAllCallCountingStubs includes a safety-net reset for Stage::Complete

Updated components:
- callcounting.cpp: All SetCodeEntryPoint paths, CompleteCallCounting,
  StopAllCallCounting, and DeleteAllCallCountingStubs
- callcounting.h: Documentation clarifying precode lifecycle invariant
- prestub.cpp: DoBackpatch comment documenting the two-state precode rule
Copilot AI review requested due to automatic review settings February 25, 2026 00:43
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

@davidwrighton davidwrighton marked this pull request as ready for review February 25, 2026 21:02
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.

2 participants