Eliminate forwarder stubs by reusing method precodes for call counting indirection#124664
Open
davidwrighton wants to merge 4 commits intodotnet:mainfrom
Open
Eliminate forwarder stubs by reusing method precodes for call counting indirection#124664davidwrighton wants to merge 4 commits intodotnet:mainfrom
davidwrighton wants to merge 4 commits intodotnet:mainfrom
Conversation
…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.
Contributor
|
Tagging subscribers to this area: @agocke |
Contributor
There was a problem hiding this comment.
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.
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:
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.