Faster ConstructorInfo.Invoke (#86855)
authorMichal Strehovský <MichalStrehovsky@users.noreply.github.com>
Mon, 29 May 2023 21:06:42 +0000 (06:06 +0900)
committerGitHub <noreply@github.com>
Mon, 29 May 2023 21:06:42 +0000 (14:06 -0700)
* Faster ConstructorInfo.Invoke

Speeds up reflection-invoking constructors by about 30%.

Noticed we spend some time in the `ConstructorInfo` invocation infrastructure for no good reason.

* We'd first allocate an instance of the object using a general purpose allocator (that first needs to dissect the `MethodTable` to figure out how to allocate), and then
* We'd call into general-purpose Invoke infrastructure that would validate the `this` is valid.

Neither of those are necessary - we can figure out the right allocator at the time the method invoker is first accessed, and validating `this` is not necessary because we _just_ allocated the right one.

* This is reachable and the old behavior was needed

* Apply suggestions from code review

---------

Co-authored-by: Jan Kotas <jkotas@microsoft.com>
14 files changed:
src/coreclr/nativeaot/System.Private.CoreLib/src/Internal/IntrinsicSupport/ComparerHelpers.cs
src/coreclr/nativeaot/System.Private.CoreLib/src/Internal/IntrinsicSupport/EqualityComparerHelpers.cs
src/coreclr/nativeaot/System.Private.CoreLib/src/Internal/Reflection/Core/Execution/ExecutionEnvironment.cs
src/coreclr/nativeaot/System.Private.CoreLib/src/Internal/Reflection/Core/Execution/MethodInvoker.cs
src/coreclr/nativeaot/System.Private.CoreLib/src/Internal/Runtime/Augments/RuntimeAugments.cs
src/coreclr/nativeaot/System.Private.CoreLib/src/System/ActivatorImplementation.cs
src/coreclr/nativeaot/System.Private.CoreLib/src/System/Reflection/Runtime/MethodInfos/CustomMethodInvoker.cs
src/coreclr/nativeaot/System.Private.CoreLib/src/System/Reflection/Runtime/MethodInfos/OpenMethodInvoker.cs
src/coreclr/nativeaot/System.Private.CoreLib/src/System/Reflection/Runtime/MethodInfos/RuntimePlainConstructorInfo.cs
src/coreclr/nativeaot/System.Private.Reflection.Execution/src/Internal/Reflection/Execution/ExecutionEnvironmentImplementation.Runtime.cs
src/coreclr/nativeaot/System.Private.Reflection.Execution/src/Internal/Reflection/Execution/MethodInvokers/InstanceMethodInvoker.cs
src/coreclr/nativeaot/System.Private.Reflection.Execution/src/Internal/Reflection/Execution/MethodInvokers/StaticMethodInvoker.cs
src/coreclr/nativeaot/System.Private.Reflection.Execution/src/Internal/Reflection/Execution/MethodInvokers/VirtualMethodInvoker.cs
src/coreclr/nativeaot/System.Private.TypeLoader/src/Internal/Runtime/TypeLoader/EETypeCreator.cs

index 16cae63..db284d8 100644 (file)
@@ -88,7 +88,7 @@ namespace Internal.IntrinsicSupport
                 Environment.FailFast("Unable to create comparer");
             }
 
-            return RuntimeAugments.NewObject(comparerType);
+            return RuntimeAugments.RawNewObject(comparerType);
         }
 
         // This one is an intrinsic that is used to make enum comparisons more efficient.
index 10ece10..dc63cad 100644 (file)
@@ -91,7 +91,7 @@ namespace Internal.IntrinsicSupport
                 Environment.FailFast("Unable to create comparer");
             }
 
-            return RuntimeAugments.NewObject(comparerType);
+            return RuntimeAugments.RawNewObject(comparerType);
         }
 
         //-----------------------------------------------------------------------
index cc30340..cf93e61 100644 (file)
@@ -27,7 +27,6 @@ namespace Internal.Reflection.Core.Execution
         //==============================================================================================
         // Access to the underlying execution engine's object allocation routines.
         //==============================================================================================
-        public abstract object NewObject(RuntimeTypeHandle typeHandle);
         public abstract Array NewArray(RuntimeTypeHandle typeHandleForArrayType, int count);
         public abstract Array NewMultiDimArray(RuntimeTypeHandle typeHandleForArrayType, int[] lengths, int[] lowerBounds);
 
index 8363b2b..a208715 100644 (file)
@@ -29,7 +29,19 @@ namespace Internal.Reflection.Core.Execution
             System.Diagnostics.DebugAnnotations.PreviousCallContainsDebuggerStepInCode();
             return result;
         }
+
+        [DebuggerGuidedStepThrough]
+        public object CreateInstance(object?[] arguments, Binder? binder, BindingFlags invokeAttr, CultureInfo? cultureInfo)
+        {
+            BinderBundle binderBundle = binder.ToBinderBundle(invokeAttr, cultureInfo);
+            bool wrapInTargetInvocationException = (invokeAttr & BindingFlags.DoNotWrapExceptions) == 0;
+            object result = CreateInstance(arguments, binderBundle, wrapInTargetInvocationException);
+            System.Diagnostics.DebugAnnotations.PreviousCallContainsDebuggerStepInCode();
+            return result;
+        }
+
         protected abstract object? Invoke(object? thisObject, object?[]? arguments, BinderBundle binderBundle, bool wrapInTargetInvocationException);
+        protected abstract object CreateInstance(object?[]? arguments, BinderBundle binderBundle, bool wrapInTargetInvocationException);
         public abstract Delegate CreateDelegate(RuntimeTypeHandle delegateType, object target, bool isStatic, bool isVirtual, bool isOpen);
 
         // This property is used to retrieve the target method pointer. It is used by the RuntimeMethodHandle.GetFunctionPointer API
index 5c00748..9d46a80 100644 (file)
@@ -67,33 +67,8 @@ namespace Internal.Runtime.Augments
         //==============================================================================================
 
         //
-        // Perform the equivalent of a "newobj", but without invoking any constructors. Other than the MethodTable, the result object is zero-initialized.
-        //
-        // Special cases:
-        //
-        //    Strings: The .ctor performs both the construction and initialization
-        //      and compiler special cases these.
-        //
-        //    Nullable<T>: the boxed result is the underlying type rather than Nullable so the constructor
-        //      cannot truly initialize it.
-        //
-        //    In these cases, this helper returns "null" and ConstructorInfo.Invoke() must deal with these specially.
-        //
-        public static object NewObject(RuntimeTypeHandle typeHandle)
-        {
-            EETypePtr eeType = typeHandle.ToEETypePtr();
-            if (eeType.IsNullable
-                || eeType == EETypePtr.EETypePtrOf<string>()
-               )
-                return null;
-            if (eeType.IsByRefLike)
-                throw new System.Reflection.TargetException();
-            return RuntimeImports.RhNewObject(eeType);
-        }
-
-        //
         // Helper API to perform the equivalent of a "newobj" for any MethodTable.
-        // Unlike the NewObject API, this is the raw version that does not special case any MethodTable, and should be used with
+        // This is the raw version that does not special case any MethodTable, and should be used with
         // caution for very specific scenarios.
         //
         public static object RawNewObject(RuntimeTypeHandle typeHandle)
index 366a70f..c4c9c1e 100644 (file)
@@ -31,7 +31,14 @@ namespace System
             if (constructor == null)
             {
                 if (type.IsValueType)
-                    return RuntimeAugments.NewObject(type.TypeHandle);
+                {
+                    RuntimeTypeHandle typeHandle = type.TypeHandle;
+
+                    if (RuntimeAugments.IsNullable(typeHandle))
+                        return null;
+
+                    return RuntimeAugments.RawNewObject(typeHandle);
+                }
 
                 throw new MissingMethodException(SR.Format(SR.Arg_NoDefCTor, type));
             }
@@ -77,7 +84,14 @@ namespace System
             if (matches.Count == 0)
             {
                 if (numArgs == 0 && type.IsValueType)
-                    return RuntimeAugments.NewObject(type.TypeHandle);
+                {
+                    RuntimeTypeHandle typeHandle = type.TypeHandle;
+
+                    if (RuntimeAugments.IsNullable(typeHandle))
+                        return null;
+
+                    return RuntimeAugments.RawNewObject(typeHandle);
+                }
 
                 throw new MissingMethodException(SR.Format(SR.Arg_NoDefCTor, type));
             }
index d0b3cab..6d71dac 100644 (file)
@@ -48,6 +48,13 @@ namespace System.Reflection.Runtime.MethodInfos
             return result;
         }
 
+        protected sealed override object CreateInstance(object?[]? arguments, BinderBundle binderBundle, bool wrapInTargetInvocationException)
+        {
+            // Custom method invokers need to also create the instance, so we just pass a null this.
+            Debug.Assert((_options & InvokerOptions.AllowNullThis) != 0);
+            return Invoke(null, arguments, binderBundle, wrapInTargetInvocationException);
+        }
+
         public sealed override Delegate CreateDelegate(RuntimeTypeHandle delegateType, object target, bool isStatic, bool isVirtual, bool isOpen)
         {
             if (_thisType.IsConstructedGenericType && _thisType.GetGenericTypeDefinition() == typeof(Nullable<>))
index 7066a5b..cf81ce9 100644 (file)
@@ -19,6 +19,11 @@ namespace System.Reflection.Runtime.MethodInfos
             throw new InvalidOperationException(SR.Arg_UnboundGenParam);
         }
 
+        protected sealed override object CreateInstance(object?[]? arguments, BinderBundle binderBundle, bool wrapInTargetInvocationException)
+        {
+            throw new InvalidOperationException(SR.Arg_UnboundGenParam);
+        }
+
         public sealed override Delegate CreateDelegate(RuntimeTypeHandle delegateType, object target, bool isStatic, bool isVirtual, bool isOpen)
         {
             throw new InvalidOperationException(SR.Arg_UnboundGenParam);
index 8a53336..a4a8fd6 100644 (file)
@@ -78,14 +78,9 @@ namespace System.Reflection.Runtime.MethodInfos
         [DebuggerGuidedStepThrough]
         public sealed override object Invoke(BindingFlags invokeAttr, Binder? binder, object?[]? parameters, CultureInfo? culture)
         {
-            // Most objects are allocated by NewObject and their constructors return "void". But in many frameworks,
-            // there are "weird" cases (e.g. String) where the constructor must do both the allocation and initialization.
-            // Reflection.Core does not hardcode these special cases. It's up to the ExecutionEnvironment to steer
-            // us the right way by coordinating the implementation of NewObject and MethodInvoker.
-            object newObject = ReflectionCoreExecution.ExecutionEnvironment.NewObject(this.DeclaringType.TypeHandle);
-            object ctorAllocatedObject = this.MethodInvoker.Invoke(newObject, parameters, binder, invokeAttr, culture)!;
+            object ctorAllocatedObject = this.MethodInvoker.CreateInstance(parameters, binder, invokeAttr, culture);
             System.Diagnostics.DebugAnnotations.PreviousCallContainsDebuggerStepInCode();
-            return newObject ?? ctorAllocatedObject;
+            return ctorAllocatedObject;
         }
 
         public sealed override MethodBase MetadataDefinitionMethod
index 00a1f29..31eb0cf 100644 (file)
@@ -21,11 +21,6 @@ namespace Internal.Reflection.Execution
     //==========================================================================================================
     internal sealed partial class ExecutionEnvironmentImplementation : ExecutionEnvironment
     {
-        public sealed override object NewObject(RuntimeTypeHandle typeHandle)
-        {
-            return RuntimeAugments.NewObject(typeHandle);
-        }
-
         public sealed override Array NewArray(RuntimeTypeHandle typeHandleForArrayType, int count)
         {
             return RuntimeAugments.NewArray(typeHandleForArrayType, count);
index 0d06122..ad03236 100644 (file)
@@ -4,6 +4,7 @@
 using global::System;
 using global::System.Threading;
 using global::System.Reflection;
+using System.Runtime.CompilerServices;
 using System.Runtime.InteropServices;
 using global::System.Diagnostics;
 using global::System.Collections.Generic;
@@ -18,12 +19,29 @@ namespace Internal.Reflection.Execution.MethodInvokers
     //
     // Implements Invoke() for non-virtual instance methods.
     //
-    internal sealed class InstanceMethodInvoker : MethodInvokerWithMethodInvokeInfo
+    internal sealed unsafe class InstanceMethodInvoker : MethodInvokerWithMethodInvokeInfo
     {
         public InstanceMethodInvoker(MethodInvokeInfo methodInvokeInfo, RuntimeTypeHandle declaringTypeHandle)
             : base(methodInvokeInfo)
         {
             _declaringTypeHandle = declaringTypeHandle;
+
+            if (methodInvokeInfo.Method.IsConstructor && !methodInvokeInfo.Method.IsStatic)
+            {
+                if (RuntimeAugments.IsByRefLike(declaringTypeHandle))
+                {
+                    _allocatorMethod = &ThrowTargetException;
+                }
+                else
+                {
+                    _allocatorMethod = (delegate*<nint, object>)RuntimeAugments.GetAllocateObjectHelperForType(declaringTypeHandle);
+                }
+            }
+        }
+
+        private static object ThrowTargetException(IntPtr _)
+        {
+            throw new TargetException();
         }
 
         [DebuggerGuidedStepThroughAttribute]
@@ -44,6 +62,20 @@ namespace Internal.Reflection.Execution.MethodInvokers
             return result;
         }
 
+        [DebuggerGuidedStepThroughAttribute]
+        protected sealed override object CreateInstance(object[] arguments, BinderBundle binderBundle, bool wrapInTargetInvocationException)
+        {
+            object thisObject = RawCalliHelper.Call<object>(_allocatorMethod, _declaringTypeHandle.Value);
+            MethodInvokeInfo.Invoke(
+                thisObject,
+                MethodInvokeInfo.LdFtnResult,
+                arguments,
+                binderBundle,
+                wrapInTargetInvocationException);
+            System.Diagnostics.DebugAnnotations.PreviousCallContainsDebuggerStepInCode();
+            return thisObject;
+        }
+
         public sealed override Delegate CreateDelegate(RuntimeTypeHandle delegateType, object target, bool isStatic, bool isVirtual, bool isOpen)
         {
             if (isOpen)
@@ -74,5 +106,13 @@ namespace Internal.Reflection.Execution.MethodInvokers
         public sealed override IntPtr LdFtnResult => MethodInvokeInfo.LdFtnResult;
 
         private RuntimeTypeHandle _declaringTypeHandle;
+        private delegate*<nint, object> _allocatorMethod;
+
+        private static class RawCalliHelper
+        {
+            [MethodImpl(MethodImplOptions.AggressiveInlining)]
+            public static T Call<T>(delegate*<IntPtr, T> pfn, IntPtr arg)
+            => pfn(arg);
+        }
     }
 }
index 74b9972..3e7a71a 100644 (file)
@@ -36,6 +36,11 @@ namespace Internal.Reflection.Execution.MethodInvokers
             return result;
         }
 
+        protected sealed override object CreateInstance(object[] arguments, BinderBundle binderBundle, bool wrapInTargetInvocationException)
+        {
+            throw NotImplemented.ByDesign;
+        }
+
         public sealed override IntPtr LdFtnResult => MethodInvokeInfo.LdFtnResult;
     }
 }
index bd4cc39..09c3c53 100644 (file)
@@ -79,6 +79,11 @@ namespace Internal.Reflection.Execution.MethodInvokers
             return result;
         }
 
+        protected sealed override object CreateInstance(object[] arguments, BinderBundle binderBundle, bool wrapInTargetInvocationException)
+        {
+            throw NotImplemented.ByDesign;
+        }
+
         internal IntPtr ResolveTarget(RuntimeTypeHandle type)
         {
             return OpenMethodResolver.ResolveMethod(MethodInvokeInfo.VirtualResolveData, type);
index 85db576..20e5377 100644 (file)
@@ -369,7 +369,7 @@ namespace Internal.Runtime.TypeLoader
                 if (state.GcDataSize != 0)
                 {
                     // Statics are allocated on GC heap
-                    object obj = RuntimeAugments.NewObject(((MethodTable*)state.GcStaticDesc)->ToRuntimeTypeHandle());
+                    object obj = RuntimeAugments.RawNewObject(((MethodTable*)state.GcStaticDesc)->ToRuntimeTypeHandle());
                     gcStaticData = RuntimeAugments.RhHandleAlloc(obj, GCHandleType.Normal);
 
                     pEEType->DynamicGcStaticsData = gcStaticData;