Activator.CreateInstance vs Compiled Expressions in C#: .NET 10 Performance Deep Dive
If you've ever built a dependency injection container, a serializer, a plugin loader, or any framework that needs to construct objects dynamically, you've hit the activator createinstance performance wall. The Activator.CreateInstance API is convenient, but it carries reflection overhead that compounds fast under load. Compiled expression trees offer a dramatically faster alternative -- but they come with their own complexity. In this article, we'll walk through both approaches in .NET 10, look at how to cache compiled delegates, and help you decide which tool belongs in your code.
Why Dynamic Object Instantiation Matters
Most application code creates objects with new. The compiler resolves everything at compile time, the JIT inlines the constructor call, and the result is as fast as it gets.
Frameworks don't always have that luxury. A DI container reads service registrations at startup and instantiates objects at resolve time. A serializer deserializes JSON into types it only learns about at runtime. A plugin system loads assemblies that don't exist at compile time and needs to activate types from those assemblies.
In all of these scenarios, you need to create objects from a Type reference -- not a compile-time type name. That's where Activator.CreateInstance comes in. It also happens to be where performance starts to suffer if you're not careful.
For background on how reflection works in .NET, see Reflection in C#: 4 Simple But Powerful Code Examples before diving into the instantiation-specific details here.
Activator.CreateInstance: API Overview
Activator.CreateInstance is the entry point most developers reach for first. The API is straightforward:
// Parameterless constructor
object instance = Activator.CreateInstance(typeof(MyService));
// With constructor arguments
object instance = Activator.CreateInstance(typeof(MyService), arg1, arg2);
// Generic overload
MyService instance = Activator.CreateInstance<MyService>();
The generic overload Activator.CreateInstance<T>() is slightly faster for types known at compile time because it avoids the params object[] allocation and the reflection scan for matching constructors. For fully dynamic scenarios, you'll use the non-generic path.
For a detailed walkthrough of the overloads and when each one applies, Activator.CreateInstance in C# - A Quick Rundown is the place to start.
What Activator.CreateInstance Does Internally
Every call to Activator.CreateInstance(Type) at runtime does roughly the following:
- Looks up the type's constructor metadata
- Validates that the argument list matches a constructor signature
- Invokes the constructor via an internal reflection path
Steps 1 and 2 cannot be skipped. They happen on every call. If you're activating thousands of objects per second -- a common scenario in hot deserialization paths or high-throughput DI containers -- this overhead adds up.
Pros and Cons
Pros:
- Simple one-liner API
- Works with any type, including types with inaccessible constructors (with the right binding flags)
- No setup required
Cons:
- Repeated reflection overhead on every call
- Params array allocation when passing constructor arguments
- Slower than compiled alternatives in hot paths
ConstructorInfo.Invoke: A Middle Ground
Before jumping to compiled expressions, it's worth looking at ConstructorInfo.Invoke. You get a ConstructorInfo once, cache it, and call Invoke on the cached reference:
// Get and cache once
ConstructorInfo ctor = typeof(MyService).GetConstructor(Type.EmptyTypes)!;
// Invoke repeatedly
object instance = ctor.Invoke(null);
This saves the constructor-lookup step on repeated calls. The Invoke itself still has reflection overhead, but caching ConstructorInfo does meaningfully cut the cost compared to re-doing the full Activator.CreateInstance lookup each time.
The article ConstructorInfo - How To Make Reflection in DotNet Faster for Instantiation digs into exactly this pattern and shows the performance difference between caching vs. not caching the constructor reference.
For a direct comparison of Activator.CreateInstance against Type.InvokeMember and other reflection-based instantiation strategies, see Activator.CreateInstance vs Type.InvokeMember - A Clear Winner?.
Compiled Expressions: The Fast Path
Expression trees let you build a delegate at runtime that the JIT can optimize just like regular code. The key insight is: pay the reflection cost once, get a compiled delegate, and call it as many times as you need at near-native speed.
For a parameterless constructor, the pattern looks like this:
using System.Linq.Expressions;
public static Func<object> BuildFactory(Type type)
{
// Expression.New generates a NewExpression -- a compile-time-equivalent `new T()`
NewExpression newExpr = Expression.New(type);
// Wrap it in a lambda that returns object
Expression<Func<object>> lambda = Expression.Lambda<Func<object>>(
Expression.Convert(newExpr, typeof(object)));
// Compile -- this is the expensive step, done ONCE
return lambda.Compile();
}
After Compile() runs, the returned Func<object> is a real delegate backed by JIT-generated native code. Calling it is functionally equivalent to calling new MyService() through a virtual dispatch -- fast enough for the hottest paths.
Handling Constructor Parameters
When the constructor takes arguments, you need to match the parameter types:
public static Func<object[], object> BuildFactory(Type type, Type[] paramTypes)
{
ConstructorInfo ctor = type.GetConstructor(paramTypes)
?? throw new InvalidOperationException($"No matching constructor found on {type.Name}");
// A single `object[]` parameter expression
ParameterExpression argsParam = Expression.Parameter(typeof(object[]), "args");
// Map each element of args[] to the correct constructor parameter type
Expression[] ctorArgs = paramTypes
.Select((paramType, i) =>
(Expression)Expression.Convert(
Expression.ArrayIndex(argsParam, Expression.Constant(i)),
paramType))
.ToArray();
Expression body = Expression.Convert(
Expression.New(ctor, ctorArgs),
typeof(object));
return Expression.Lambda<Func<object[], object>>(body, argsParam).Compile();
}
This delegate takes an object[] and returns the constructed instance. The Convert nodes handle the unboxing of each argument from object to the actual parameter type.
The FuncCache Pattern
Compiling an expression is expensive -- roughly equivalent to a full reflection scan plus JIT compilation. You never want to compile the same factory twice. The solution is a static dictionary keyed on Type:
public static class ActivatorCache
{
private static readonly ConcurrentDictionary<Type, Func<object>> _cache = new();
public static object Create(Type type) =>
_cache.GetOrAdd(type, t =>
{
var ctor = t.GetConstructor(Type.EmptyTypes)
?? throw new InvalidOperationException($"No parameterless constructor on {t.Name}");
var body = Expression.New(ctor);
return Expression.Lambda<Func<object>>(body).Compile();
})();
}
ConcurrentDictionary.GetOrAdd is thread-safe and ensures the factory is compiled exactly once per type. Available since .NET 8 and fully supported in .NET 10, FrozenDictionary is an option once all types are registered at startup and the cache becomes read-only -- more on that pattern in a related article on caching reflection metadata.
Typed Generic Variant
If you know the return type at the call site, a generic variant avoids boxing entirely:
public static class ActivatorCache<T> where T : class
{
private static readonly Func<T> _factory = BuildFactory();
public static T CreateInstance() => _factory();
private static Func<T> BuildFactory()
{
NewExpression newExpr = Expression.New(typeof(T));
Expression<Func<T>> lambda = Expression.Lambda<Func<T>>(newExpr);
return lambda.Compile();
}
}
The static field initializer runs exactly once per generic instantiation. No locking needed, no dictionary lookup needed. This is the fastest option when T is known at the call site.
When to Use Which Approach
Not every situation calls for a compiled expression. Here's a practical guide:
| Scenario | Recommended Approach |
|---|---|
| One-time object creation in test setup | Activator.CreateInstance |
| Framework startup, no hot path | ConstructorInfo (cached) |
| High-frequency DI resolution | Compiled expression + delegate cache |
| Generic factory, type known at call site | ActivatorCache<T> static field |
| Private constructor (test/reflection tool) | UnsafeAccessor or ConstructorInfo with NonPublic flags |
| Source-available types, maximum speed | Source generator (no reflection at all) |
The crossover point where compiled expressions pay off depends on call frequency. If a factory is called fewer than ~100 times in the process lifetime, the compilation cost may not be worth it. If it's called in a per-request path under load, compilation pays back quickly.
Setting Up the Benchmark
BenchmarkDotNet makes it easy to measure the difference. Here's the structure to use -- run it against your actual types to get real numbers for your workload:
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using System.Linq.Expressions;
using System.Reflection;
[MemoryDiagnoser]
[RankColumn]
public class InstantiationBenchmarks
{
private static readonly ConstructorInfo _ctor =
typeof(MyService).GetConstructor(Type.EmptyTypes)!;
private static readonly Func<object> _compiledFactory =
Expression.Lambda<Func<object>>(
Expression.Convert(Expression.New(typeof(MyService)), typeof(object)))
.Compile();
[Benchmark(Baseline = true)]
public object Activator_CreateInstance()
=> Activator.CreateInstance(typeof(MyService))!;
[Benchmark]
public object ConstructorInfo_Invoke()
=> _ctor.Invoke(null)!;
[Benchmark]
public object CompiledExpression()
=> _compiledFactory();
[Benchmark]
public MyService Generic_CreateInstance()
=> Activator.CreateInstance<MyService>();
}
public sealed class MyService { }
Run this with BenchmarkRunner.Run<InstantiationBenchmarks>() in a Release build. The [MemoryDiagnoser] attribute will also show you allocations per operation -- which matters as much as time in GC-sensitive paths.
UnsafeAccessor: The .NET 8+ Alternative for Private Constructors
.NET 8 introduced UnsafeAccessor, available since .NET 8 and fully supported in .NET 10.
Note:
[UnsafeAccessor]targets compile-time-known private members and constructors -- it is not a substitute for dynamic type instantiation. See the dedicated UnsafeAccessor article for correct usage.
FAQ
What is Activator.CreateInstance and when should I use it?
Activator.CreateInstance is the .NET API for creating objects dynamically at runtime from a Type reference. Use it when you need one-off object creation in tooling, test infrastructure, or low-frequency paths where performance is not a concern. For hot paths, switch to a compiled expression delegate.
How much faster are compiled expressions than Activator.CreateInstance?
Compiled expression delegates can be significantly faster than repeated uncached reflection in hot paths -- but the actual ratio depends heavily on your types, call frequency, and workload. Benchmark with BenchmarkDotNet to find the right threshold for your scenario.
Is it safe to share a compiled factory delegate across threads?
Yes. Once a delegate is compiled and the Func<T> reference is stored in a field, it's a read-only reference to immutable JIT code. Calling the delegate from multiple threads is safe as long as the constructor itself doesn't have thread-safety issues.
Why not always use the generic Activator.CreateInstance<T>()?
The generic overload only works when T is known at compile time. In truly dynamic scenarios -- where the type comes from a Type variable loaded at runtime -- you cannot use the generic overload. The non-generic path is your only option there, and that's where compiled expressions help.
What is the FuncCache pattern?
FuncCache is the pattern of storing compiled Func<T> delegates in a dictionary keyed by Type, so each type's factory is compiled exactly once. Subsequent calls retrieve the cached delegate and invoke it directly without any reflection. The pattern is thread-safe when using ConcurrentDictionary<Type, Func<object>> with GetOrAdd, or FrozenDictionary for read-only caches built at startup.
Does .NET 10 change anything about Activator.CreateInstance performance?
.NET 10 continues the runtime JIT improvements started in .NET 6-9, and the Dynamic PGO (Profile-Guided Optimization) can help devirtualize some reflection paths. However, Activator.CreateInstance still performs reflection on every call. Compiled expressions are still the right answer for high-frequency paths.
How do compiled expressions relate to source generators?
Both are ways to avoid runtime reflection overhead. Compiled expressions do the work at runtime -- once, on first use. Source generators do the work at compile time and emit real C# code that has zero reflection overhead at runtime. Source generators are faster but require your types to be known and source-available at compile time. See Source Generation vs Reflection in Needlr for a real-world comparison of the two strategies.
Conclusion
Activator.CreateInstance is a great starting point for dynamic object creation, but it pays a reflection tax on every call. ConstructorInfo.Invoke with caching reduces that cost. Compiled expression trees eliminate the per-call reflection overhead entirely -- at the cost of a one-time compilation step.
The right choice comes down to call frequency. Low frequency? Activator.CreateInstance is fine. High frequency, performance-sensitive path? Cache a compiled delegate and call that instead. The ActivatorCache<T> generic pattern takes it a step further by eliminating even the dictionary lookup for types known at the call site.
In .NET 10, these patterns remain the dominant tools for high-performance dynamic instantiation. Run BenchmarkDotNet against your real types, measure allocation pressure alongside time, and pick the approach that fits your workload.

