-
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
API proposal: Activator.CreateFactory and ConstructorInfo.CreateDelegate #36194
Comments
Related runtime/src/libraries/Common/src/Extensions/ActivatorUtilities/ActivatorUtilities.cs Line 106 in 23e13a3
Here's the ObjectFactory definition
|
Yeah, that's the part I'm having trouble with. What you're talking about is the more general case of fast method invocation. I'm blanking right now on a good, reliable, high-performance way to do that without involving codegen. Right now Let's assume for the moment that you had a good way of invoking arbitrary instance methods in a high-performance fashion. The APIs proposed here would allow you to extend that same concept to constructors of arbitrary signature. To do this, you'd use the proposed Edit: As an example of what I said above, assume that the pattern you wanted to support instantiating was public class MyFactory
{
private Func<object> _allocator;
private Action<object, IServiceProvider> _ctor;
public MyFactory(Type targetType)
{
if (targetType.IsValueType) { /* throw */ }
_allocator = RuntimeHelpers.GetUninitializedObjectFactory(targetType);
MethodInfo mi = targetType.GetMethod(".ctor", new Type[] { typeof(IServiceProvider) });
_ctor = (Action<object, IServiceProvider>)mi.CreateDelegate(typeof(Action<object, IServiceProvider>), null /* instance */);
}
public object CreateInstance(IServiceProvider serviceProvider)
{
object instance = _allocator(); // these two calls together are basically 'newobj'
_ctor(instance, serviceProvider);
return instance;
}
} In this scenario there's zero codegen involved. If you wanted to support value types it gets a bit more complicated. Maybe that's something we could help with here. |
I'm trying to understand the benefit of this over private static class Cache<T> { public static readonly Func<T> Func = CreateFactory<T>(); }
public static T CreateInstance<T>() => Cache<T>.Func(); ? I'm sure I'm just missing something. |
I do plan on adding some of the existing For applications which maintain their own caching mechanisms we can return to them a factory which bypasses all of the built in indirection. This is the most desirable from the perspective of giving them bare metal performance so that there's no need for them to use their own codegen. Edit: activator doesn't even initialize the cache until the second call to the constructor. The reason is that we want to optimize for the case where the type is instantiated only once, perhaps as part of app startup. |
Do we also need an overload without (Also, we need to be thinking about the linker annotations when introducing new reflection APIs.)
For the generic overload, the fundamental advantage of the factory method compared to For the non-generic overload, the additional fundamental advantage of the factory method compared to |
We probably should, for convenience and to help lessen confusion.
Do you have pointers on this topic? I don't see any annotations on the existing |
#36229 . It is work in progress. These annotations were discussed during the API review meetings a few weeks ago. |
I see. It's unfortunate we pay that overhead, in particular in the |
Right. Second reason for not recommending this API to C# compiler is that Roslyn would need to generate a boilerplate to cache the delegate. The factory method adds an extra frame as well compared to just calling |
Using
|
Only in specific situations, such as using the generic overload from another generic context? Otherwise I don't see that.. I just tried this: using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using System;
[MemoryDiagnoser]
public class Program
{
static void Main(string[] args) => BenchmarkSwitcher.FromAssemblies(new[] { typeof(Program).Assembly }).Run(args);
[Benchmark] public Program A1() => Activator.CreateInstance<Program>();
[Benchmark] public object A2() => Activator.CreateInstance(typeof(Program));
[Benchmark] public Program A3() => (Program)Activator.CreateInstance(typeof(Program));
} and got this:
Since you can't currently write such a T in C#, I'm not very concerned about optimizing for that case. It also seems like something we should be able to fix if it was really meaningful. |
In practice should the comparison be with |
That is because of the current implementation of E.g. run this:
CoreCLR:
CoreRT:
|
namespace System
{
public partial class Activator
{
// Existing APIs
// public static T CreateInstance<T>();
// public static object? CreateInstance(Type type);
// public static object? CreateInstance(Type type, bool nonPublic);
public static Func<T> CreateFactory<T>();
public static Func<object?> CreateFactory(Type type);
public static Func<object?> CreateFactory(Type type, bool nonPublic);
}
}
namespace System.Reflection
{
public class ConstructorInfo
{
public static TDelegate CreateDelegate<TDelegate>();
public static Delegate CreateDelegate(Type delegateType);
}
} |
FWIW, we can play some games within For example, say that you have these three call sites: private object? MySharedDelegate(ref JsonSerializerOptions options);
Func<string, object> factory1 = ci1.CreateDelegate<Func<string, object>>();
Func<IServiceProvider, IService> factory2 = ci2.CreateDelegate<Func<IServiceProvider, IService>>();
MySharedDelegate factory3 = (MySharedDelegate)ci3.CreateDelegate(typeof(MySharedDelegate)); We'd only need to spin up the JIT once to handle all of these cases, as our parameter shuffling thunk can be shared between all factory signatures. Normal ref emit-based consumers can't make this same optimization. |
Right, the guidelines talk about public APIs, but they indirectly affect what natural internal implementations looks like in many cases. Using different guidelines for internal implementations would often be inconsistent and add unnecessary overhead from back-and-forth conversions. You can reduce the burden of repeating the full signature everywhere via |
@GrabYourPitchforks feel free to change milestone back if you're planning to do this in this release |
Doesn't seem we could make it within 7.0 |
I think it makes sense to close this issue. For v8, we added These new APIs also support fixed-parameter arguments to avoid the However, they are based on emit once called a second time so they are not code-less except as mentioned above for NativeAOT. Also see #78917 which proposes having existing reflection APIs fall back to Activator for the fast cases which do not require emit. |
(See also #32520.)
API proposal
Discussion
Instantiating objects whose types are not known until runtime is fairly common practice among frameworks and libraries. Deserializers perform this task frequently. Web frameworks and DI frameworks might create per-request instances of objects, then destroy those objects at the end of the request.
APIs like
Activator.CreateInstance
and the System.Reflection surface area can help with this to a large extent. However, those are sometimes seen as heavyweight solutions. We've built some caching mechanisms into the framework to suppose these use cases. But it's still fairly common for high-performance frameworks to bypassActivator
and the reflection stack and to go straight to manual codegen. See below for some examples.See below for some concrete samples.
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
Ref emit incurs a substantial upfront perf hit, but it does generally provide a win amortized over the lifetime of the cached method as it's invoked over and over again. However, this comes with its own set of problems. It's difficult for developers to get the exact IL correct across the myriad edge cases that might exist. It's not very memory-efficient. And as runtimes that don't allow codegen become more commonplace, it complicates the callers' code to have to decide the best course of action to take for any given runtime.
These proposed APIs attempt to solve the problem of creating a basic object factory using the best mechanism applicable to the current runtime. The exact mechanism used can vary based on runtime: perhaps it's codegen, perhaps it's reflection, perhaps it's something else. But the idea is that the performance of these APIs should rival the best hand-rolled implementations that library authors can create.
Shortcomings, not solved here
This API is not a panacea to address all performance concerns developers have with the reflection stack. For example, this won't change the perf characteristics of
MethodInfo.Invoke
. But it could be used to speed up the existingActivator.CreateInstance<T>
APIs and to make other targeted improvements. In general, this API provides an alternative pattern that developers can use so that they don't have to roll solutions themselves.It also does not fully address the concern of calls to parameterized ctors, such as you might find in DI systems. The API
RuntimeHelpers.GetUninitializedObjectFactory
does help with this to some extent. The caller can cache that factory to instantiate "blank" objects quickly, then use whatever existing fast mechanism they wish to call the object's real ctor over the newly allocated instance.I hope to have a better solution for this specific scenario in a future issue.
Stretch goal APIs
The APIs
ConstructorInfo.CreateDelegate<TDelegate>()
and friends are meant to allow invoking parameterless or parameterful ctors. These are primarily useful when the objects you're constructing have a common signature in their constructors, but you don't know the actual type of the object at compile time.Consider:
In these cases, the caller would get the
ConstructorInfo
they care about, then callconstructorInfo.CreateDelegate<Func<IServiceProvider, object>>()
. Depending on whichConstructorInfo
was provided, the returned delegate will create either aMyService
or aMyOtherService
, calling the appropriate ctor with the caller-providedIServiceProvider
.I say "stretch goal" because the parameter shuffling involved here would involve spinning up the JIT. We can keep this overhead to a minimum because within the runtime we can take shortcuts that non-runtime components can't take with the typical
DynamicMethod
-based way of doing things. So while there's less JIT cost compared to a standardDynamicMethod
-based solution, there's still some JIT cost, so it doesn't fully eliminate startup overhead. (This overhead isn't much different than the overhead the runtime already incurs when it sees code likeFunc<string, bool> func = string.IsNullOrEmpty
;, since the runtime already needs to create a parameter shuffling thunk for this scenario.)The text was updated successfully, but these errors were encountered: