Skip to content

Commit ffa24bd

Browse files
Improve performance of Activator.CreateInstance (#32520)
- Use modern C# calli features to invoke allocator and ctor - Share arg validation code between CreateInstance and GetUninitializedObject - Improve exception message when CreateInstance fails - Lay foundation for future work in Activator Co-authored-by: Jan Kotas <[email protected]>
1 parent 1503364 commit ffa24bd

File tree

13 files changed

+496
-340
lines changed

13 files changed

+496
-340
lines changed

src/coreclr/src/System.Private.CoreLib/System.Private.CoreLib.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,7 @@
220220
<Compile Include="$(BclSourcesRoot)\System\Runtime\Versioning\CompatibilitySwitch.cs" />
221221
<Compile Include="$(BclSourcesRoot)\System\RuntimeArgumentHandle.cs" />
222222
<Compile Include="$(BclSourcesRoot)\System\RuntimeHandles.cs" />
223+
<Compile Include="$(BclSourcesRoot)\System\RuntimeType.ActivatorCache.cs" />
223224
<Compile Include="$(BclSourcesRoot)\System\RuntimeType.CoreCLR.cs" />
224225
<Compile Include="$(BclSourcesRoot)\System\Security\DynamicSecurityMethodAttribute.cs" />
225226
<Compile Include="$(BclSourcesRoot)\System\StartupHookProvider.cs" />

src/coreclr/src/System.Private.CoreLib/src/System/RuntimeHandles.cs

Lines changed: 45 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -208,12 +208,6 @@ internal static bool HasElementType(RuntimeType type)
208208
return outHandles;
209209
}
210210

211-
[MethodImpl(MethodImplOptions.InternalCall)]
212-
internal static extern object CreateInstance(RuntimeType type, bool publicOnly, bool wrapExceptions, ref bool canBeCached, ref RuntimeMethodHandleInternal ctor, ref bool hasNoDefaultCtor);
213-
214-
[MethodImpl(MethodImplOptions.InternalCall)]
215-
internal static extern object Allocate(RuntimeType type);
216-
217211
internal static object CreateInstanceForAnotherGenericParameter(RuntimeType type, RuntimeType genericParameter)
218212
{
219213
object? instantiatedObject = null;
@@ -258,6 +252,51 @@ private static extern void CreateInstanceForAnotherGenericParameter(
258252
int cTypeHandles,
259253
ObjectHandleOnStack instantiatedObject);
260254

255+
/// <summary>
256+
/// Given a RuntimeType, returns information about how to activate it via calli
257+
/// semantics. This method will ensure the type object is fully initialized within
258+
/// the VM, but it will not call any static ctors on the type.
259+
/// </summary>
260+
internal static void GetActivationInfo(
261+
RuntimeType rt,
262+
out delegate*<void*, object> pfnAllocator,
263+
out void* vAllocatorFirstArg,
264+
out delegate*<object, void> pfnCtor,
265+
out bool ctorIsPublic)
266+
{
267+
Debug.Assert(rt != null);
268+
269+
delegate*<void*, object> pfnAllocatorTemp = default;
270+
void* vAllocatorFirstArgTemp = default;
271+
delegate*<object, void> pfnCtorTemp = default;
272+
Interop.BOOL fCtorIsPublicTemp = default;
273+
274+
GetActivationInfo(
275+
ObjectHandleOnStack.Create(ref rt),
276+
&pfnAllocatorTemp, &vAllocatorFirstArgTemp,
277+
&pfnCtorTemp, &fCtorIsPublicTemp);
278+
279+
pfnAllocator = pfnAllocatorTemp;
280+
vAllocatorFirstArg = vAllocatorFirstArgTemp;
281+
pfnCtor = pfnCtorTemp;
282+
ctorIsPublic = fCtorIsPublicTemp != Interop.BOOL.FALSE;
283+
}
284+
285+
[DllImport(RuntimeHelpers.QCall, CharSet = CharSet.Unicode)]
286+
private static extern void GetActivationInfo(
287+
ObjectHandleOnStack pRuntimeType,
288+
delegate*<void*, object>* ppfnAllocator,
289+
void** pvAllocatorFirstArg,
290+
delegate*<object, void>* ppfnCtor,
291+
Interop.BOOL* pfCtorIsPublic);
292+
293+
#if FEATURE_COMINTEROP
294+
// Referenced by unmanaged layer (see GetActivationInfo).
295+
// First parameter is ComClassFactory*.
296+
[MethodImpl(MethodImplOptions.InternalCall)]
297+
private static extern object AllocateComObject(void* pClassFactory);
298+
#endif
299+
261300
internal RuntimeType GetRuntimeType()
262301
{
263302
return m_type;
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Diagnostics;
5+
using System.Diagnostics.CodeAnalysis;
6+
using System.Runtime.CompilerServices;
7+
8+
namespace System
9+
{
10+
internal sealed partial class RuntimeType
11+
{
12+
/// <summary>
13+
/// A cache which allows optimizing <see cref="Activator.CreateInstance"/>,
14+
/// <see cref="RuntimeType.CreateInstanceDefaultCtor"/>, and related APIs.
15+
/// </summary>
16+
private sealed unsafe class ActivatorCache
17+
{
18+
// The managed calli to the newobj allocator, plus its first argument (MethodTable*).
19+
// In the case of the COM allocator, first arg is ComClassFactory*, not MethodTable*.
20+
private readonly delegate*<void*, object?> _pfnAllocator;
21+
private readonly void* _allocatorFirstArg;
22+
23+
// The managed calli to the parameterless ctor, taking "this" (as object) as its first argument.
24+
private readonly delegate*<object?, void> _pfnCtor;
25+
private readonly bool _ctorIsPublic;
26+
27+
#if DEBUG
28+
private readonly RuntimeType _originalRuntimeType;
29+
#endif
30+
31+
internal ActivatorCache(RuntimeType rt)
32+
{
33+
Debug.Assert(rt != null);
34+
35+
#if DEBUG
36+
_originalRuntimeType = rt;
37+
#endif
38+
39+
// The check below is redundant since these same checks are performed at the
40+
// unmanaged layer, but this call will throw slightly different exceptions
41+
// than the unmanaged layer, and callers might be dependent on this.
42+
43+
rt.CreateInstanceCheckThis();
44+
45+
try
46+
{
47+
RuntimeTypeHandle.GetActivationInfo(rt,
48+
out _pfnAllocator!, out _allocatorFirstArg,
49+
out _pfnCtor!, out _ctorIsPublic);
50+
}
51+
catch (Exception ex)
52+
{
53+
// Exception messages coming from the runtime won't include
54+
// the type name. Let's include it here to improve the
55+
// debugging experience for our callers.
56+
57+
string friendlyMessage = SR.Format(SR.Activator_CannotCreateInstance, rt, ex.Message);
58+
switch (ex)
59+
{
60+
case ArgumentException: throw new ArgumentException(friendlyMessage);
61+
case PlatformNotSupportedException: throw new PlatformNotSupportedException(friendlyMessage);
62+
case NotSupportedException: throw new NotSupportedException(friendlyMessage);
63+
case MethodAccessException: throw new MethodAccessException(friendlyMessage);
64+
case MissingMethodException: throw new MissingMethodException(friendlyMessage);
65+
case MemberAccessException: throw new MemberAccessException(friendlyMessage);
66+
}
67+
68+
throw; // can't make a friendlier message, rethrow original exception
69+
}
70+
71+
// Activator.CreateInstance returns null given typeof(Nullable<T>).
72+
73+
if (_pfnAllocator == null)
74+
{
75+
Debug.Assert(Nullable.GetUnderlyingType(rt) != null,
76+
"Null allocator should only be returned for Nullable<T>.");
77+
78+
static object? ReturnNull(void* _) => null;
79+
_pfnAllocator = &ReturnNull;
80+
}
81+
82+
// If no ctor is provided, we have Nullable<T>, a ctorless value type T,
83+
// or a ctorless __ComObject. In any case, we should replace the
84+
// ctor call with our no-op stub. The unmanaged GetActivationInfo layer
85+
// would have thrown an exception if 'rt' were a normal reference type
86+
// without a ctor.
87+
88+
if (_pfnCtor == null)
89+
{
90+
static void CtorNoopStub(object? uninitializedObject) { }
91+
_pfnCtor = &CtorNoopStub; // we use null singleton pattern if no ctor call is necessary
92+
93+
Debug.Assert(_ctorIsPublic); // implicit parameterless ctor is always considered public
94+
}
95+
96+
// We don't need to worry about invoking cctors here. The runtime will figure it
97+
// out for us when the instance ctor is called. For value types, because we're
98+
// creating a boxed default(T), the static cctor is called when *any* instance
99+
// method is invoked.
100+
}
101+
102+
internal bool CtorIsPublic => _ctorIsPublic;
103+
104+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
105+
internal object? CreateUninitializedObject(RuntimeType rt)
106+
{
107+
// We don't use RuntimeType, but we force the caller to pass it so
108+
// that we can keep it alive on their behalf. Once the object is
109+
// constructed, we no longer need the reference to the type instance,
110+
// as the object itself will keep the type alive.
111+
112+
#if DEBUG
113+
if (_originalRuntimeType != rt)
114+
{
115+
Debug.Fail("Caller passed the wrong RuntimeType to this routine."
116+
+ Environment.NewLineConst + "Expected: " + (_originalRuntimeType ?? (object)"<null>")
117+
+ Environment.NewLineConst + "Actual: " + (rt ?? (object)"<null>"));
118+
}
119+
#endif
120+
121+
object? retVal = _pfnAllocator(_allocatorFirstArg);
122+
GC.KeepAlive(rt);
123+
return retVal;
124+
}
125+
126+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
127+
internal void CallConstructor(object? uninitializedObject) => _pfnCtor(uninitializedObject);
128+
}
129+
}
130+
}

src/coreclr/src/System.Private.CoreLib/src/System/RuntimeType.CoreCLR.cs

Lines changed: 25 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,6 @@
1717

1818
namespace System
1919
{
20-
// this is a work around to get the concept of a calli. It's not as fast but it would be interesting to
21-
// see how it compares to the current implementation.
22-
// This delegate will disappear at some point in favor of calli
23-
24-
internal delegate void CtorDelegate(object instance);
25-
2620
// Keep this in sync with FormatFlags defined in typestring.h
2721
internal enum TypeNameFormatFlags
2822
{
@@ -3968,113 +3962,45 @@ private void CreateInstanceCheckThis()
39683962
return instance;
39693963
}
39703964

3971-
// the cache entry
3972-
private sealed class ActivatorCache
3973-
{
3974-
// the delegate containing the call to the ctor
3975-
internal readonly RuntimeMethodHandleInternal _hCtorMethodHandle;
3976-
internal MethodAttributes _ctorAttributes;
3977-
internal CtorDelegate? _ctor;
3978-
3979-
// Lazy initialization was performed
3980-
internal volatile bool _isFullyInitialized;
3981-
3982-
private static ConstructorInfo? s_delegateCtorInfo;
3983-
3984-
internal ActivatorCache(RuntimeMethodHandleInternal rmh)
3985-
{
3986-
_hCtorMethodHandle = rmh;
3987-
}
3988-
3989-
private void Initialize()
3990-
{
3991-
if (!_hCtorMethodHandle.IsNullHandle())
3992-
{
3993-
_ctorAttributes = RuntimeMethodHandle.GetAttributes(_hCtorMethodHandle);
3994-
3995-
// The default ctor path is optimized for reference types only
3996-
ConstructorInfo delegateCtorInfo = s_delegateCtorInfo ??= typeof(CtorDelegate).GetConstructor(new Type[] { typeof(object), typeof(IntPtr) })!;
3997-
3998-
// No synchronization needed here. In the worst case we create extra garbage
3999-
_ctor = (CtorDelegate)delegateCtorInfo.Invoke(new object?[] { null, RuntimeMethodHandle.GetFunctionPointer(_hCtorMethodHandle) });
4000-
}
4001-
_isFullyInitialized = true;
4002-
}
4003-
4004-
public void EnsureInitialized()
4005-
{
4006-
if (!_isFullyInitialized)
4007-
Initialize();
4008-
}
4009-
}
4010-
40113965
/// <summary>
4012-
/// The slow path of CreateInstanceDefaultCtor
3966+
/// Helper to invoke the default (parameterless) constructor.
40133967
/// </summary>
4014-
private object? CreateInstanceDefaultCtorSlow(bool publicOnly, bool wrapExceptions, bool fillCache)
3968+
[DebuggerStepThrough]
3969+
[DebuggerHidden]
3970+
internal object? CreateInstanceDefaultCtor(bool publicOnly, bool skipCheckThis, bool fillCache, bool wrapExceptions)
40153971
{
4016-
RuntimeMethodHandleInternal runtimeCtor = default;
4017-
bool canBeCached = false;
4018-
bool hasNoDefaultCtor = false;
3972+
// Get or create the cached factory. Creating the cache will fail if one
3973+
// of our invariant checks fails; e.g., no appropriate ctor found.
3974+
//
3975+
// n.b. In coreclr we ignore 'skipCheckThis' (assumed to be false)
3976+
// and 'fillCache' (assumed to be true).
40193977

4020-
object instance = RuntimeTypeHandle.CreateInstance(this, publicOnly, wrapExceptions, ref canBeCached, ref runtimeCtor, ref hasNoDefaultCtor);
4021-
if (hasNoDefaultCtor)
3978+
if (GenericCache is not ActivatorCache cache)
40223979
{
4023-
throw new MissingMethodException(SR.Format(SR.Arg_NoDefCTor, this));
3980+
cache = new ActivatorCache(this);
3981+
GenericCache = cache;
40243982
}
40253983

4026-
if (canBeCached && fillCache)
3984+
if (!cache.CtorIsPublic && publicOnly)
40273985
{
4028-
// cache the ctor
4029-
GenericCache = new ActivatorCache(runtimeCtor);
3986+
throw new MissingMethodException(SR.Format(SR.Arg_NoDefCTor, this));
40303987
}
40313988

4032-
return instance;
4033-
}
3989+
// Compat: allocation always takes place outside the try block so that OOMs
3990+
// bubble up to the caller; the ctor invocation is within the try block so
3991+
// that it can be wrapped in TIE if needed.
40343992

4035-
/// <summary>
4036-
/// Helper to invoke the default (parameterless) constructor.
4037-
/// </summary>
4038-
[DebuggerStepThrough]
4039-
[DebuggerHidden]
4040-
internal object? CreateInstanceDefaultCtor(bool publicOnly, bool skipCheckThis, bool fillCache, bool wrapExceptions)
4041-
{
4042-
// Call the cached
4043-
if (GenericCache is ActivatorCache cacheEntry)
3993+
object? obj = cache.CreateUninitializedObject(this);
3994+
try
40443995
{
4045-
cacheEntry.EnsureInitialized();
4046-
4047-
if (publicOnly)
4048-
{
4049-
if (cacheEntry._ctor != null &&
4050-
(cacheEntry._ctorAttributes & MethodAttributes.MemberAccessMask) != MethodAttributes.Public)
4051-
{
4052-
throw new MissingMethodException(SR.Format(SR.Arg_NoDefCTor, this));
4053-
}
4054-
}
4055-
4056-
// Allocate empty object and call the default constructor if present.
4057-
object instance = RuntimeTypeHandle.Allocate(this);
4058-
Debug.Assert(cacheEntry._ctor != null || IsValueType);
4059-
if (cacheEntry._ctor != null)
4060-
{
4061-
try
4062-
{
4063-
cacheEntry._ctor(instance);
4064-
}
4065-
catch (Exception e) when (wrapExceptions)
4066-
{
4067-
throw new TargetInvocationException(e);
4068-
}
4069-
}
4070-
4071-
return instance;
3996+
cache.CallConstructor(obj);
3997+
}
3998+
catch (Exception e) when (wrapExceptions)
3999+
{
4000+
throw new TargetInvocationException(e);
40724001
}
40734002

4074-
if (!skipCheckThis)
4075-
CreateInstanceCheckThis();
4076-
4077-
return CreateInstanceDefaultCtorSlow(publicOnly, wrapExceptions, fillCache);
4003+
return obj;
40784004
}
40794005

40804006
internal void InvalidateCachedNestedType() => Cache.InvalidateCachedNestedType();

src/coreclr/src/vm/corelib.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -371,6 +371,9 @@ DEFINE_CLASS(RT_TYPE_HANDLE, System, RuntimeTypeHandle)
371371
DEFINE_METHOD(RT_TYPE_HANDLE, GET_TYPE_HELPER, GetTypeHelper, SM_Type_ArrType_IntPtr_int_RetType)
372372
DEFINE_METHOD(RT_TYPE_HANDLE, PVOID_CTOR, .ctor, IM_RuntimeType_RetVoid)
373373
DEFINE_METHOD(RT_TYPE_HANDLE, GETVALUEINTERNAL, GetValueInternal, SM_RuntimeTypeHandle_RetIntPtr)
374+
#ifdef FEATURE_COMINTEROP
375+
DEFINE_METHOD(RT_TYPE_HANDLE, ALLOCATECOMOBJECT, AllocateComObject, SM_VoidPtr_RetObj)
376+
#endif
374377
DEFINE_FIELD(RT_TYPE_HANDLE, M_TYPE, m_type)
375378

376379
DEFINE_CLASS_U(Reflection, RtFieldInfo, NoClass)

src/coreclr/src/vm/ecalllist.h

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,6 @@ FCFuncStart(gSystem_RuntimeType)
189189
FCFuncEnd()
190190

191191
FCFuncStart(gCOMTypeHandleFuncs)
192-
FCFuncElement("CreateInstance", RuntimeTypeHandle::CreateInstance)
193192
QCFuncElement("CreateInstanceForAnotherGenericParameter", RuntimeTypeHandle::CreateInstanceForAnotherGenericParameter)
194193
QCFuncElement("GetGCHandle", RuntimeTypeHandle::GetGCHandle)
195194
QCFuncElement("FreeGCHandle", RuntimeTypeHandle::FreeGCHandle)
@@ -239,7 +238,10 @@ FCFuncStart(gCOMTypeHandleFuncs)
239238
FCFuncElement("IsGenericTypeDefinition", RuntimeTypeHandle::IsGenericTypeDefinition)
240239
FCFuncElement("ContainsGenericVariables", RuntimeTypeHandle::ContainsGenericVariables)
241240
FCFuncElement("SatisfiesConstraints", RuntimeTypeHandle::SatisfiesConstraints)
242-
FCFuncElement("Allocate", RuntimeTypeHandle::Allocate) //for A.CI
241+
QCFuncElement("GetActivationInfo", RuntimeTypeHandle::GetActivationInfo)
242+
#ifdef FEATURE_COMINTEROP
243+
FCFuncElement("AllocateComObject", RuntimeTypeHandle::AllocateComObject)
244+
#endif // FEATURE_COMINTEROP
243245
FCFuncElement("CompareCanonicalHandles", RuntimeTypeHandle::CompareCanonicalHandles)
244246
FCIntrinsic("GetValueInternal", RuntimeTypeHandle::GetValueInternal, CORINFO_INTRINSIC_RTH_GetValueInternal)
245247
FCFuncElement("IsEquivalentTo", RuntimeTypeHandle::IsEquivalentTo)

src/coreclr/src/vm/metasig.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -464,6 +464,7 @@ DEFINE_METASIG(IM(RefObject_RetBool, r(j), F))
464464
DEFINE_METASIG_T(IM(Class_RetObj, C(CLASS), j))
465465
DEFINE_METASIG(IM(Int_VoidPtr_RetVoid, i P(v), v))
466466
DEFINE_METASIG(IM(VoidPtr_RetVoid, P(v), v))
467+
DEFINE_METASIG(SM(VoidPtr_RetObj, P(v), j))
467468

468469
DEFINE_METASIG_T(IM(Str_RetModule, s, C(MODULE)))
469470
DEFINE_METASIG_T(SM(Assembly_Str_RetAssembly, C(ASSEMBLY) s, C(ASSEMBLY)))

0 commit comments

Comments
 (0)