-
Notifications
You must be signed in to change notification settings - Fork 4.8k
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
Improve performance of Activator.CreateInstance #32520
Improve performance of Activator.CreateInstance #32520
Conversation
Note regarding the This serves as a reminder for when your PR is modifying a ref *.cs file and adding/modifying public APIs, to please make sure the API implementation in the src *.cs file is documented with triple slash comments, so the PR reviewers can sign off that change. |
It should also be feasible to write codegen-less stubs for some public static class ObjectFactory
{
public static Func<T1, TObject> CreateFactory<TObject, T1>(ConstructorInfo ctor);
public static Func<T1, T2, TObject> CreateFactory<TObject, T1, T2>(ConstructorInfo ctor);
// ...
public static Func<T1, T2, /* ..., */ T16, TObject> CreateFactory<TObject, T1, T2, /* ..., */ T16>(ConstructorInfo ctor);
} I'm not quite sure at the moment how to support this pattern for arbitrary delegate types without involving codegen. In an ideal world I'd be able to expose a generic method as below. public static class ObjectFactory
{
public static TDelegate CreateFactory<TDelegate>(ConstructorInfo ctor) where TDelegate : delegate;
}
private delegate ICommonInterface CreationDelegate(int a, long b, string c);
CreationDelegate del = ObjectFactory.CreateFactory<CreationDelegate>(someConcreteType.GetConstructor(/* ... */));
ICommonInterface newlyCreatedObj = del(40, 0xdeadbeef, "hello!"); As long we we don't have Edit: Pointers can be smuggled as IntPtr, so that shouldn't be a problem. Refs and byrefs are still a problem. It means we couldn't expose a delegate over the following ctor. public class MyClass : ICommonInterface
{
public MyClass(ReadOnlySpan<char> data);
}
private delegate ICommonInterface Factory(ReadOnlySpan<char> data);
Factory factory = ObjectFactory.CreateFactory<Factory>(/* some ConstructorInfo */); // fails at runtime |
This seems immensely valuable for ASP.NET Core (and I'm sure scores of other libraries), especially if we can get this include more arities, and more kinds of things beyond construction. One more thing that I want to push onto your stack:
We end up needing this kind of thing a lot - BUT in the scenario where we can't write compile-time type-safe code. We end up with If we feel like these problems can only be solved adequately with build-time codegen, then it's possible that the things I'm mentioning here aren't super relevant. |
src/libraries/System.Private.CoreLib/src/System/Reflection/ObjectFactory.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Private.CoreLib/src/System/Reflection/ObjectFactory.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Private.CoreLib/src/System/Reflection/UninitializedObjectFactory.cs
Outdated
Show resolved
Hide resolved
src/coreclr/src/System.Private.CoreLib/src/System/RuntimeType.CoreCLR.cs
Outdated
Show resolved
Hide resolved
Both examples are weakly typed (return object). What is the example for the strongly typed consumer of this?
Right, build-time codegen is the most optimal thing for this. |
Possible scenario (though it would require API tweaks): // calculate and cache these upfront
Type actualServiceType = environment.GetConcreteTypeForAbstraction(typeof(IMyService));
ObjectFactory<IMyService> factory = ObjectFactory.Create<IMyService>(actualServiceType);
// call this in a hot path, such as per-request
IMyService service = factory.CreateInstance(); Or, for an MVC-like framework's per-request processing logic: Type controllerType = GetControllerTypeForCurrentRequest();
ObjectFactory<Controller> factory = s_factories[controllerType];
Controller controller = factory.CreateInstance();
Console.WriteLine(controller.GetType()); // could be AdminController, WeatherController, etc. There's nothing stopping us from saying " |
If we are designing a new activator-like API we should ensure linking and AOT scenarios needs are addressed. |
Once you tweak the API like this, you do not need the non-generic version. The non-generic version is equivalent to |
I'm not actively working on this PR right now. Closing it here so that it doesn't distract the folks who maintain this repo. It's being tracked at GrabYourPitchforks#1 so it doesn't fall entirely off the radar. |
Rewrote the prototype using the C# native function pointers feature. Here's the codegen for 00007ffd`7410d030 57 push rdi ; prolog
00007ffd`7410d031 56 push rsi
00007ffd`7410d032 4883ec28 sub rsp,28h
00007ffd`7410d036 488bf1 mov rsi,rcx
00007ffd`7410d039 488b4618 mov rax,qword ptr [rsi+18h] ; rax := pfnNewHelper
00007ffd`7410d03d 488b4e10 mov rcx,qword ptr [rsi+10h] ; rcx := pMT
00007ffd`7410d041 ffd0 call rax
00007ffd`7410d043 488bf8 mov rdi,rax
00007ffd`7410d046 488b4620 mov rax,qword ptr [rsi+20h] ; rax := pfnParameterlessCtor
00007ffd`7410d04a 488bcf mov rcx,rdi ; rcx := return value from pfnNewHelper
00007ffd`7410d04d ffd0 call rax
00007ffd`7410d04f 488bc7 mov rax,rdi ; rax := fully initialized object instance
00007ffd`7410d052 4883c428 add rsp,28h ; epilog
00007ffd`7410d056 5e pop rsi
00007ffd`7410d057 5f pop rdi
00007ffd`7410d058 c3 ret
|
And here are the perf results from the latest prototype, taking everybody's feedback into account. The first number shows a significant boost to
The second number compares the cost of invoking a factory to perform activation. In the first row, the
This shows that the difference is less than half a nanosecond. Since the factory returned by And just in case people were interested in how long it takes to compile a
|
- Eliminates ObjectFactory<T> except when caller explicitly wants a Func<T> - Uses C# 9.0 function pointers to avoid introducing new JIT intrinsics
I reopened this because it's actively being worked on. Still not ready for review because of build failures, lack of support for C# 9.0 language features, and no mono integration yet. It hasn't gone through more than basic smoke testing yet. |
911d7bd
to
d0bde83
Compare
A status update - I'm still working on this but keep running into issues where the linker either crashes or trims a method which is clearly statically reachable. Trying to work around those before sending up a new iteration. Meanwhile I've reduced the scope of the work so that it's not adding any new APIs in the 5.0 timeframe. |
- Remove new public APIs - Remove most new native APIs - Plumb GetUninitializedObject atop new native code paths - Use actual builds from arcade
I think all pending PR feedback is addressed, including [largely] restoring the original unmanaged method for The file was also renamed as part of this, so I'm hoping GitHub's diff tool can correctly highlight only the lines that changed. There was some earlier feedback about debug assertions. I currently have an assertion in each of the APIs |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks great! Thank you
Co-authored-by: Jan Kotas <[email protected]>
Intermediate commit applied only your suggestions without my fixes to balance out braces, etc. Should be good now. |
👍 |
Test failing due to the newly-added asserts: https://helixre107v0xdeko0k025g8.blob.core.windows.net/dotnet-runtime-refs-pull-32520-merge-1988d13847ae470481/Interop/console.4eecf050.log?sv=2019-07-07&se=2020-12-16T04%3A21%3A35Z&sr=c&sp=rl&sig=TQivJ0aRUNLD3%2FXXQozy90NnIEQZhnk1Nk0aeAoxzqI%3D Interop\COM\ComWrappers\GlobalInstance\GlobalInstanceMarshallingTests\GlobalInstanceMarshallingTests.cmd [FAIL]
Process terminated. Assertion failed.
Caller passed an unexpected 'this' parameter to ctor - possible type safety violation.
Expected type: System.__ComObject
Actual type: ComWrappersTests.GlobalInstance.Program+FakeWrapper
at System.RuntimeType.CreateInstanceDefaultCtor(Boolean publicOnly, Boolean skipCheckThis, Boolean fillCache, Boolean wrapExceptions)
at System.Activator.CreateInstance(Type type, Boolean nonPublic, Boolean wrapExceptions)
at System.Activator.CreateInstance(Type type)
at ComWrappersTests.GlobalInstance.Program.ValidateNativeServerActivation() in /__w/1/s/src/tests/Interop/COM/ComWrappers/GlobalInstance/GlobalInstance.cs:line 391
at ComWrappersTests.GlobalInstance.Program.ValidateComActivation(Boolean validateUseRegistered) in /__w/1/s/src/tests/Interop/COM/ComWrappers/GlobalInstance/GlobalInstance.cs:line 380
at ComWrappersTests.GlobalInstance.Program.Main(String[] doNotUse) in /__w/1/s/src/tests/Interop/COM/ComWrappers/GlobalInstance/GlobalInstance.Marshalling.cs:line 35 Trying to figure out whether the assert indicates a real problem or whether the assert should be removed. Edit: looks like the runtime behavior is intentional due to the presence of a custom COM marshaler in the sample app. runtime/src/tests/Interop/COM/ComWrappers/GlobalInstance/GlobalInstance.cs Lines 146 to 168 in 1503364
I think it's best to remove the assert. |
@jkotas Do you suppose there's any benefit to eagerly JITting the ctor before we obtain its fnptr? We're about to turn around and invoke it anyway, so may as well get the fnptr directly to the target method rather than to the prestub? Perhaps this would make more sense for the |
There is still going to be an extra indirection by default because of tiered JIT and other features like profiler rejit. We pay for these indirections in many other places. The way to fix them is to register the place where the function pointer is stored for backpatching. e.g. the backpatching would have to aware of |
Any reasons for still having |
Nah. I'll remove the label and merge in just a sec. Wanted to go over it with Steve early next week once the holiday is finished, but honestly any extra feedback can go in as a follow up PR. Thank you again for your patience and for sharing your expertise. :) |
catch (Exception ex) | ||
{ | ||
// Exception messages coming from the runtime won't include | ||
// the type name. Let's include it here to improve the | ||
// debugging experience for our callers. | ||
|
||
string friendlyMessage = SR.Format(SR.Activator_CannotCreateInstance, rt, ex.Message); | ||
switch (ex) | ||
{ | ||
case ArgumentException: throw new ArgumentException(friendlyMessage); | ||
case PlatformNotSupportedException: throw new PlatformNotSupportedException(friendlyMessage); | ||
case NotSupportedException: throw new NotSupportedException(friendlyMessage); | ||
case MethodAccessException: throw new MethodAccessException(friendlyMessage); | ||
case MissingMethodException: throw new MissingMethodException(friendlyMessage); | ||
case MemberAccessException: throw new MemberAccessException(friendlyMessage); | ||
} | ||
|
||
throw; // can't make a friendlier message, rethrow original exception | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this better than, say:
catch (ArgumentException e) { throw new ArgumentException(CreateFriendlyMessage(rt, e)); }
catch (PlatformNotSupportedException e) { throw new PlatformNotSupportedException(CreateFriendlyMessage(rt, e)); }
... // etc.
? I realize the above is probably a bit more IL, but it also means you won't catch and rethrow if the exception didn't match one of the special-cased types. I don't know how common that will be.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not ignoring this, but leaving this thread marked unresolved for now because I think we can address it as part of #36194.
@GrabYourPitchforks the improvement has been confirmed in DrewScoggins/performance-2#3541 |
Contributes to #23716.
This is a very rough prototype of
ObjectFactory
andUninitializedObjectFactory
, which allow fast codegen-free instantiation of arbitrary objects. It's primarily suited for DI and other scenarios where developers need to instantiate objects whose types aren't known until runtime but are required to do so with as minimal overhead as possible.Historically, the way to instantiate such objects is to call
Activator.CreateInstance
orConstructorInfo.Invoke
. However, there is some overhead to this, as checks need to take place on each reflection API invocation. Some developers who need to eke out extra performance end up creating aDynamicMethod
and hand-rolling a newobj instruction into the IL stream. This provides generally acceptable results, but it is tricky to get right, there's significant per-instantiation overhead due to spinning up the JIT, it's complex to debug issues in the code gen, and it doesn't work properly in environments where code gen is unavailable.Note: These APIs have not yet been approved, as this is just an experimental PR to gauge general interest and to solicit feedback on the design and functionality.
General philosophy
Historically, most reflection APIs like
Activator.CreateInstance
andConstructorInfo.Invoke
perform checks on each method invocation. We can try to reduce the cost of these checks, but due to the nature of these APIs we'll never be able to remove the checks entirely.The
UninitializedObjectFactory
andObjectFactory
APIs, on the other hand, perform all of their checks at construction time, not at invocation time. This means that it may be somewhat expensive to create the factory / dispenser itself, but once the factory is created it's very cheap to request a new instance from the factory. The intent is that these factories aren't used for one-off scenarios - callers should continue to useActivator.CreateInstance
andConstructorInfo.Invoke
in such code paths. But in scenarios where the same type of object will be instantiated over and over again, it's very efficient to cache a single instance of the factory per-type and to query the cached instance whenever a newT
instance is needed.UninitializedObjectFactory
The
UninitializedObjectFactory
type is roughly equivalent toRuntimeHelpers.GetUninitializedObject
. This instantiates the type without running any instance constructors. (Static constructors are still run.) Most applications should never need this, but it is useful for library authors building their own serialization or DI frameworks on top of this. The general pattern such library authors should follow is to useUninitializedObjectFactory
to create a not-yet-constructed instance of target typeT
, then manually invoke the appropriate constructor on the newly-allocated instance.ObjectFactory
The
ObjectFactory
type is roughly equivalent toActivator.CreateInstance<T>()
. It allocates the type then calls the public, parameterless instance constructor. There are two main differences betweenObjectFactory.CreateInstance
andActivator.CreateInstance<T>()
:ObjectFactory.CreateInstance
will not wrap the thrown exception within aTargetInvocationException
.Activator.CreateInstance<T>()
will wrap exceptions.T
is a value type,ObjectFactory.CreateInstance
will always returndefault(T)
.Activator.CreateInstance<T>()
will callT
's parameterless constructor if one exists; otherwise it returnsdefault(T)
.Potential consumers
System.Text.Json
runtime/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReflectionEmitMemberAccessor.cs
Lines 39 to 51 in 9d85b82
ASP.NET Core
dotnet/aspnetcore#14615 (though they're using
TypeBuilder
to work around this right now)Other runtime + libraries
runtime/src/libraries/System.ComponentModel.Composition/src/System/ComponentModel/Composition/MetadataViewGenerator.cs
Lines 371 to 380 in 9d85b82
Inner workings
A
newobj
instruction is logically two operations: (a) allocate a block of zero-inited memory, then (b) call the type's instance ctor over this block of memory. (UninitializedObjectFactory
skips this second step.)When using
Activator.CreateInstance
, the runtime uses a generic allocator that can handle many different scenarios for allocating an object whose type is known only at runtime. And while this generic allocator is flexible, there's significant overhead due to all the different checks it needs to perform.On the other hand, when JITting a
newobj
instruction, since the JIT knows the destination type statically, it's able to choose from a variety of allocators whose implementations are optimized for various different scenarios. For example, there exists an allocator tailored for small, non-finalizable types. There exists a different allocator tailored for finalizable types. And there exists yet another allocator that's used when instrumentation is enabled.When an
[Uninitialized]ObjectFactory<T>
instance is created, it queries the runtime for the address of the allocator that would have been used for an object of type T. All allocators share the signature(MethodTable*) -> O
, so once the allocator address is determined we can perform the equivalent of the following IL in order to quickly allocate the object.If we need to fully construct the object, then we further query the runtime for the address of the code that corresponds to the public parameterless ctor of type T. All public parameterless ctors on reference types share the signature
(object) -> void
, so once the ctor address is determined we can perform the equivalent of the following IL in order to quickly call the ctor.Putting this all together, we get the following pseudo-codegen for
ObjectFactory<T>.CreateInstance
.Performance
Below are preliminary numbers for various "create an
object
" scenarios. Callingnew object()
directly is the baseline.new object()
directlyDynamicMethod
whose IL stream readsnewobj [object]; ret;
ObjectFactory<object>
'sCreateInstance
methodI've also experimented with plumbing this through the existing
Activator.CreateInstance
pipeline. The perf numbers are not as good as callingObjectFactory.CreateInstance
directly because there's still overhead to querying the static cache and performing any necessary checks, and there's some compat behavior such asTargetInvocationException
wrapping that we can't avoid, but they still show a significant improvement over the baseline (master branch)Activator.CreateInstance
numbers.And for
RuntimeHelpers.GetUninitializedObject(typeof(object))
vs.UninitializedObjectFactory<object>.CreateUninitializedInstance()
:Remaining work
new T(IServiceProvider)
.calli
support when it's natively available in C#.