JIT: use type argument as the exact type for Activator.CreateInstance<T>#130074
Conversation
|
Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch |
|
@MihuBot -nuget |
There was a problem hiding this comment.
Pull request overview
This PR extends CoreCLR JIT intrinsic recognition so calls to System.Activator.CreateInstance<T>() can be treated as producing an instance of the exact type argument T, enabling downstream optimizations (e.g., devirtualization) when the type argument is known.
Changes:
- Adds a new named intrinsic
NI_System_Activator_CreateInstance_T. - Recognizes
Activator.CreateInstancewith a type argument inlookupNamedIntrinsicand marks it as a “special intrinsic” call site. - Teaches
gtGetClassHandleto recover the exactTforCreateInstance<T>calls to sharpen return type information.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| src/coreclr/jit/namedintrinsiclist.h | Adds a new NamedIntrinsic enum value for Activator.CreateInstance<T>. |
| src/coreclr/jit/importercalls.cpp | Recognizes Activator.CreateInstance<T> as a named intrinsic and flags it as “special” for later optimizations. |
| src/coreclr/jit/gentree.cpp | Attempts to infer the exact return type of Activator.CreateInstance<T> in gtGetClassHandle. |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.
Comments suppressed due to low confidence (1)
src/coreclr/jit/importercalls.cpp:10706
lookupNamedIntrinsicnow unconditionally callseeGetMethodSig(method, &sig)even though the signature is only used for theActivator.CreateInstancecheck. SincelookupNamedIntrinsicruns for every intrinsic call import, this adds extra EE work on a hot import path. Consider fetching the signature only inside theActivator/CreateInstancebranch (or using a cheaper generic-method check) so most intrinsics avoid the extraeeGetMethodSigcall.
info.compCompHnd->getMethodNameFromMetadata(method, &className, &namespaceName, enclosingClassNames,
ArrLen(enclosingClassNames));
JITDUMP("Named Intrinsic ");
Is this also applicable to COM imported types? |
Unlike For example: using System.Runtime.InteropServices;
internal static class Program
{
[STAThread]
static void Main()
{
ShellWindows windows = new ShellWindows(); // changing to `Activator.CreateInstance<ShellWindows>()` will result in a runtime exception
Console.WriteLine(windows.Count);
}
}
[ComImport]
[Guid("9BA05972-F6A8-11CF-A442-00A0C90A8F39")]
internal class ShellWindowsClass
{
}
[ComImport]
[Guid("85CB6900-4D95-11CF-960C-0080C7F4EE85")]
[CoClass(typeof(ShellWindowsClass))]
[InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
internal interface ShellWindows
{
[DispId(0x60020000)]
int Count { get; }
} |
jakobbotsch
left a comment
There was a problem hiding this comment.
This LGTM. @EgorBo what is your opinion?
|
LGTM, but to be honest, |
|
BTW, it looks like there are no hits in BCL so it's basically an untested bit of code? |
|
We do have many coverages: https://github.com/search?q=repo%3Adotnet%2Fruntime+%22Activator.CreateInstance%3C%22+OR+repo%3Adotnet%2Fruntime+%2Fwhere.*%3F%3A.*%3Fnew%5C%28%5C%29%2F&type=code It's not showing up in the spmi because it enables more devirtualization so we end up with context mismatches. |
|
Waiting on #130099 before merging. |
|
I think instead of walking through the call tree, I can just simply spill the call and mark the store as single def local with the exact class. @jakobbotsch What do you think? |
Would it simplify anything? I think in the end it will look about the same but move the recognition one higher level up than necessary. Ideally places that need this information use |
Okay, let's keep it as is. Is there anything else I need to do before this can be merged? |
No, I think this looks good. Thanks! |
…<T> (#130074) `Activator.CreateInstance<T>` can only result in an instance of exact `T`. Teach the JIT so that when the JIT sees a call to `Activator.CreateInstance<T>`, it will know the result must be the exact `T`. Then any subsequent calls against the allocated object instance can be devirtualized. This also applies to any `where T : new()` patterns. Example: ```cs class Program { static T Get<T>() where T : new() { return new T(); } class A { public virtual int Value => 1; } class B : A { public override int Value => 2; } static void Main() { Console.WriteLine(Get<A>().Value); Console.WriteLine(Get<B>().Value); } } ``` Before: ```nasm G_M27646_IG01: ;; offset=0x0000 sub rsp, 40 G_M27646_IG02: ;; offset=0x0004 mov rcx, 0x7FF92B6AB428 ; System.Activator:CreateInstance[Program+A]():Program+A call [System.Activator:CreateInstance[System.__Canon]():System.__Canon] mov rcx, rax mov rax, qword ptr [rax] mov rax, qword ptr [rax+0x48] call [rax+0x20]Program+A:get_Value():int:this mov ecx, eax call [System.Console:WriteLine(int)] mov rcx, 0x7FF92B6AB890 ; System.Activator:CreateInstance[Program+B]():Program+B call [System.Activator:CreateInstance[System.__Canon]():System.__Canon] mov rcx, rax mov rax, qword ptr [rax] mov rax, qword ptr [rax+0x48] call [rax+0x20]Program+A:get_Value():int:this mov ecx, eax call [System.Console:WriteLine(int)] nop G_M27646_IG03: ;; offset=0x004F add rsp, 40 ret ``` After: ```nasm G_M27646_IG01: ;; offset=0x0000 sub rsp, 40 G_M27646_IG02: ;; offset=0x0004 mov rcx, 0x7FF92C79B428 ; System.Activator:CreateInstance[Program+A]():Program+A call [System.Activator:CreateInstance[System.__Canon]():System.__Canon] mov ecx, 1 call [System.Console:WriteLine(int)] mov rcx, 0x7FF92C79B890 ; System.Activator:CreateInstance[Program+B]():Program+B call [System.Activator:CreateInstance[System.__Canon]():System.__Canon] mov ecx, 2 call [System.Console:WriteLine(int)] nop G_M27646_IG03: ;; offset=0x003B add rsp, 40 ret ``` In the future we can apply escape analysis against `CreateInstance<T>` as well to omit unescaped object allocations.
Activator.CreateInstance<T>can only result in an instance of exactT. Teach the JIT so that when the JIT sees a call toActivator.CreateInstance<T>, it will know the result must be the exactT. Then any subsequent calls against the allocated object instance can be devirtualized.This also applies to any
where T : new()patterns.Example:
Before:
After:
In the future we can apply escape analysis against
CreateInstance<T>as well to omit unescaped object allocations.