JIT: use type argument as the exact type for Activator.CreateInstance<T>#130074
JIT: use type argument as the exact type for Activator.CreateInstance<T>#130074hez2010 wants to merge 10 commits into
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. |
| assert(sig.sigInst.classInstCount == 0); | ||
|
|
||
| CORINFO_CLASS_HANDLE typeHnd = sig.sigInst.methInst[0]; | ||
| assert(typeHnd != nullptr); |
There was a problem hiding this comment.
🤖 AI review
typeHnd is a dead assignment here. It's computed and asserted, but result is only written inside the instParam != nullptr branch, so typeHnd is never read.
Impact: for non-shared/exact instantiations there's no InstParam, so we fall through to "undetermined" and drop the opt — even though methInst[0] already is the exact T (the value-type / R2R-AOT case where it matters most).
Fix (mirrors the SZArrayHelper_GetEnumerator case above — refine typeHnd, assign once, no else):
CORINFO_CLASS_HANDLE typeHnd = sig.sigInst.methInst[0];
assert(typeHnd != nullptr);
CallArg* instParam = call->gtArgs.FindWellKnownArg(WellKnownArg::InstParam);
if (instParam != nullptr)
{
assert(instParam->GetNext() == nullptr);
CORINFO_METHOD_HANDLE hMethod = gtGetHelperArgMethodHandle(instParam->GetNode());
if (hMethod != NO_METHOD_HANDLE)
{
typeHnd = getMethodInstantiationArgument(hMethod, 0);
}
}
result = typeHnd;One thing to confirm: can methInst[0] be __Canon when InstParam is absent for CreateInstance<T>? If so, result = typeHnd; would report __Canon as exact, and the fallback should be guarded to the instParam == nullptr path instead.
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.