Skip to content

[clr-interp] Add support for virtual method calls #114529

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Apr 19, 2025

Conversation

BrzVlad
Copy link
Member

@BrzVlad BrzVlad commented Apr 11, 2025

The PR follows the suggestion of @davidwrighton (thanks a lot for the input), which makes it trivial to implement these types of calls, by reusing existing code to resolve the target MethodDesc that needs to be invoked. This should help us expand interpreter capability quite fast, given I expect it to work for pretty much all scenarios of virtual/interface calls.

Interpreter execution design is optimized for invocation based solely on interpreter IR code pointer. This means that, long term, we should switch to invocation based on method slots that are expected to contain directly the code pointer, without having to do all these lookups at MethodDesc level. This seems that it might be a bit tricky to do and I'm investigating what options we would have.

@Copilot Copilot AI review requested due to automatic review settings April 11, 2025 09:47
@BrzVlad BrzVlad requested review from janvorli and kg as code owners April 11, 2025 09:47
Copy link
Contributor

@Copilot 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.

Copilot reviewed 5 out of 6 changed files in this pull request and generated no comments.

Files not reviewed (1)
  • src/coreclr/interpreter/intops.def: Language not supported

Copy link
Contributor

Tagging subscribers to this area: @BrzVlad, @janvorli, @kg
See info in area-owners.md if you want to be subscribed.

{
pInterpreterFrame->SetTopInterpMethodContextFrame(pFrame);
GCX_PREEMP();
pMD->PrepareInitialCode(CallerGCMode::Coop);
Copy link
Member

Choose a reason for hiding this comment

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

Why is it CallerGCMode::Coop if we just did GCX_PREEMP?

Copy link
Member

Choose a reason for hiding this comment

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

This is setting the caller GC mode for the code being prepared, not the caller mode of the PrepareInitialCode.

if (isVirtual)
{
AddIns(INTOP_CALLVIRT);
m_pLastNewIns->data[0] = GetDataItemIndex(targetMethod);
Copy link
Member

Choose a reason for hiding this comment

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

Why are we using MethodDataItemIndex in one arm and DataItemIndex in the other if it's a CORINFO_METHOD_HANDLE in both cases?

Copy link
Member Author

Choose a reason for hiding this comment

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

The INTOP_CALL MethodDesc slot contains a tagged pointer. Initially it is a tagged MethodDesc, afterwards it is meant to be replaced with the code pointer. In the case of INTOP_CALLVIRT it is just a MethodDesc without any tag.

I think this should change in the future. Some of the changes might look like this: In the case of INTOP_CALL we might want to add the address of the MethodDesc precode slot (that will get patched with either a compiled or interpreter code pointer). For vtable virtual calls we might have the offsets to the vtable slot containing the code pointer. For interface calls we might maintain a cache in the instruction stream similar to VSD (helping with monomorphic calls) and also store the interface method token for resolving the method to be called as a fallback.

@jkotas
Copy link
Member

jkotas commented Apr 11, 2025

This means that, long term, we should switch to invocation based on method slots that are expected to contain directly the code pointer,

I think you want to mirror what we do for virtual method dispatch in regular JITed code. Some combination of:

  • A global hashtable that maps the method we want to dispatch and the actual type to a code pointer
    and/or
  • Per-call site cache that does the same (it can use the same datastructure as FEATURE_CACHED_INTERFACE_DISPATCH)

I do not think it is worth it to try to invent some super optimized path for regular virtual methods that uses slot indices. I think it would just create a lot of problems with keeping things unified between runtime that has interpreter support and runtime that does not have interpreter support. The caches above should be just fine. We have actually tried to use caches like above for regular virtual methods as well at one point in the past and the performance was about as good on average as the performance of invocation using slot indices.

@BrzVlad
Copy link
Member Author

BrzVlad commented Apr 11, 2025

I think normal virtual calls (that are dispatch through the vtable) should still go through the vtable in the interpreter scenario long term. A vtable slot would contain either a normal code pointer (for compiled code) or a tagged interpreter IR code pointer (for interpreted code). So this type of call would look logically identical between interp and jit. They would both load the code pointer from the slot and invoke it accordingly whether the code pointer is jit or interp. I don't see why we would want to use a different approach on interpreter (via hash tables or per call site caching). Although for interface calls I see us doing something like this, to reuse as much as possible the logic/code from VSD.

@BrzVlad
Copy link
Member Author

BrzVlad commented Apr 11, 2025

Well, I guess the obvious drawback would be that each virtual call emitted with JIT would need to check if the address to call is tagged.

@jkotas
Copy link
Member

jkotas commented Apr 11, 2025

I don't see why we would want to use a different approach on interpreter (via hash tables or per call site caching).

It will just work once you implement the general path and I expect that it will work reasonably well. We use the per call site caching for regular virtual methods in regular CoreCLR for less common cases too.

Well, I guess the obvious drawback would be that each virtual call emitted with JIT would need to check if the address to call is tagged.

Right. It is a performance trade-off. You will shift the costs around. It is not clear to me whether it would be a win at the end in the typical configuration.

@BrzVlad BrzVlad force-pushed the feature-clr-interp7 branch from feefc96 to 722b18c Compare April 12, 2025 12:14
BrzVlad added 3 commits April 14, 2025 17:52
This follows a trivial approach where we resolve the MethodDesc of the target method by reusing existing functionality. Long term, virtual dispatching should go through slots continaing interpreter IR code pointer, in a similar fashion to JIT.
No gc support yet
@BrzVlad BrzVlad force-pushed the feature-clr-interp7 branch from 3c3d613 to f367573 Compare April 14, 2025 14:52
@BrzVlad BrzVlad merged commit d874371 into dotnet:main Apr 19, 2025
102 of 104 checks passed
@BrzVlad BrzVlad mentioned this pull request Apr 3, 2025
61 tasks
@github-actions github-actions bot locked and limited conversation to collaborators May 20, 2025
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants