blog post image
Andrew Lock avatar

Andrew Lock

~11 min read

Benchmarking 4 reflection methods for calling a constructor in .NET

The typical way to create an object in .NET/C# is to use the new keyword. However it's also possible to create a new instance of an object using reflection. In this post I compare 4 different methods, and benchmark them to see which is fastest.

Creating objects using reflection—why bother?

In the Datadog APM tracer library we automatically instrument multiple libraries in your application. This allows us to add distributed tracing to your application without you having to change your code at all.

To achieve this, we often have to interact with library types without having a direct reference to them. For example, I was recently working on our Kafka integration to automatically instrument Confluent's .NET Client for Kafka. I needed to be able to create an instance of the Headers class.

In an application, the solution would be simple—reference the Confluent.Kafka NuGet package, and call new Headers(), but we can't do that in the Tracer. If we reference a NuGet package, then we would need to reference that package when we instrument your application. But what if you already reference some version of Confluent.Kafka? On .NET Framework we would likely run into binding redirect problems, while on .NET Core/.NET 5 we could cause MissingMethodExceptions or any number of other problems.

To avoid all this, we use reflection to interact with the Confluent.Kafka types, using whichever version of the library you have referenced in your application. This avoids the multiple-references issue (though obviously means we need to be careful about ensuring we test with many different versions of the library).

Reflection is hugely flexible, but the big downside is that it's slow compared to directly interacting with objects. For a performance-critical application like the APM tracer, we need to make sure we're being as performant as possible, so we ensure that the reflection we do is highly optimised.

Which brings me back to the case in point. I needed a way to call new Headers() without referencing the Headers type directly. There are actually multiple ways to do this using reflection, which I will walk through here. At the end of the post, I run a benchmark to compare them, to see which is fastest.

4 ways to create an object using reflection

There are quite possibly more, but I've come up with 4 ways to create an object using reflection:

  • Calling Invoke on a ConstructorInfo instance.
  • Using Activator.CreateInstance().
  • Creating a Compiled Expression.
  • Using DynamicMethod and Reflection.Emit.

These are arranged in roughly ascending order of complexity. I'll walk through each approach in the following sections before we get to the benchmarks themselves.

1. Standard Reflection using Invoke

The first approach is the "traditional" reflection approach:

Type typeToCreate = typeof(Headers);
ConstructorInfor ctor = typeToCreate.GetConstructor(System.Type.EmptyTypes);
object headers = ctor.Invoke(null);

The first step is to obtain a Type representing the type you want to create. In this case (and in all the other examples) I've used typeof(Headers) for simplicity, but this relies on having a direct reference to the Headers type, so in practice, you'd obtain the same Type through some other mechanism.

From this Type you can get a reference to the ConstructorInfo, which describes the parameterless constructor for the Headers type. The EmptyTypes helper is a cached empty array (new Type[0]), which is used to indicate you want the parameterless constructor.

Finally, you can execute the constructor by calling Invoke() on the ConstructorInfo and passing in null (as the constructor does not take any arguments). This returns an object which is actually an instance of Headers.

This is one of the simplest and most flexible ways to use reflection, as you can use similar approaches to invoke methods on an object, access fields, interfaces and attributes etc. However, as you'll see later, it's also one of the slowest. The next approach is a slightly optimised approach designed specifically for our scenario - construction.

2. Activator.CreateInstance

In this post I'm looking at a single scenario - creating an instance of an object. There happens to be a helper class designed specifically for this available in both .NET Framework and .NET Core called Activator. You can use this class to easily create an instance of a type using the following:

Type typeToCreate = typeof(Headers);
object headers = Activator.CreateInstance(typeToCreate);

as before, we need a reference to the Headers type, but then we can simply call Activator.CreateInstance(type). No need to mess around with ConstructorInfo or anything. Very handy! As with the previous method, you can call parameterised constructors too if you need to.

There's not much more to say about this one, so we'll move on to the next one, where things start to get more interesting.

3. Compiled expressions

Expressions have been around for a long time (since C# 3.0) and are integral to various other features and libraries such as LINQ and ORMs such as EF Core. In many ways they are similar to reflection, in that they allow manipulation of code at runtime,

Expressions offer a high-ish level language for declaring code, which can subsequently be converted into an executable Func<> by calling Compile. We can create an expression that creates an instance of the Headers type, compile it into a Func<object>, and then invoke it as follows:

NewExpression constructorExpression = Expression.New(HeadersType);
Expression<Func<object>> lambdaExpression = Expression.Lambda<Func<object>>(constructorExpression);
Func<object> createHeadersFunc = lambdaExpression.Compile();  
object Headers = createHeadersFunc();

The first two lines of this snippet create an expression that is equivalent to () => new Headers(). The third line converts the Expression<> into a Func<> that we can execute. The final line invokes our newly created Func<object> to create the Headers object.

For this simple example of calling a constructor, the syntax is pretty easy to understand, especially as you've likely worked with Expressions in the past. In contrast, the final approach in this post, using Reflection.Emit, may not be something you've used before (I hadn't!).

4. Reflection.Emit

Reflection.Emit refers to the System.Reflection.Emit namespace, which contains various methods for creating new intermediate language (IL) in your application. IL instructions are the "assembly code" that the compiler outputs when you compile your application. The JIT in the .NET runtime converts these IL instructions into real assembly code when your application runs.

The approach in this section uses a class called DynamicMethod to create a new method in the assembly at runtime, with the IL method body that creates an instance of the Headers object.

Effectively, we're dynamically creating a method that looks like this:

Headers KafkaDynamicMethodHeaders()
{
  return new Headers();
}

The following snippet creates a method signature similar to this using DynamicMethod, though it doesn't have a method body yet;

Type headersType = typeof(Headers);
DynamicMethod createHeadersMethod = new DynamicMethod(
    name: $"KafkaDynamicMethodHeaders",
    returnType: headersType,
    parametertypes: null,
    module: typeof(ConstructorBenchmarks).Module,
    skipVisibility: false);

We create a DynamicMethod providing the name of the method, the parameter and return Types, and the Module in which the method should be defined. Finally we specify whether the JIT visibility checks should be skipped for types and members accessed by the IL of the dynamic method. I'm only accessing public types here, so chose to not skip it.

Types are defined in Modules. This is very similar to an Assembly, but a single Assembly can contain multiple Modules.

We have the method signature, now we need to create the method body's IL:

ConstructorInfor ctor = typeToCreate.GetConstructor(System.Type.EmptyTypes);

ILGenerator il = createHeadersMethod.GetILGenerator();
il.Emit(OpCodes.Newobj, Ctor);
il.Emit(OpCodes.Ret);

We obtain an ILGenerator for the method by calling GetILGenerator(). We can then Emit() IL operation codes. This is essentially hand-crafting the "raw" IL codes that the original KafkaDynamicMethodHeaders would create.

One way to work out what IL you need is to write the C# you need, and then look at the generated IL.

IL_0000: newobj instance void Headers::.ctor()
IL_0005: ret

The final step to create the dynamic method is to create a delegate that we can use to execute the function. We'll use a Func<object> again, which we can invoke to create the Headers object:

Func<object> headersDelegate = createHeadersMethod.CreateDelegate(typeof(Func<object>));
object headers = headersDelegate();

That covers the 4 approaches to calling a constructor using reflection, now on to the benchmarks!

Benchmarking the approaches

To run the benchmarks, I used the excellent BenchmarkDotNet library and created the following benchmark. This uses the new Header() case as the "baseline", and compares each of the other approaches. All of the one-off work of loading Types, compiling expressions, and generating DynamicMethods is done in the constructor, so it's not included in the startup time. We're measuring the "steady state" time to create the Headers instance.

using System;
using System.Linq.Expressions;
using System.Reflection;
using System.Reflection.Emit;
using BenchmarkDotNet.Attributes;
using Confluent.Kafka;

public class ConstructorBenchmarks
{
    private static readonly Type HeadersType = typeof(Headers);
    private static readonly ConstructorInfo Ctor = HeadersType.GetConstructor(System.Type.EmptyTypes);
    private readonly Func<object> _dynamicMethodActivator;
    private readonly Func<object> _dynamicMethodActivator2;
    private readonly Func<object> _expression;

    public ConstructorBenchmarks()
    {
        DynamicMethod createHeadersMethod = new DynamicMethod(
            $"KafkaDynamicMethodHeaders",
            HeadersType,
            null,
            typeof(ConstructorBenchmarks).Module,
            false);

        ILGenerator il = createHeadersMethod.GetILGenerator();
        il.Emit(OpCodes.Newobj, Ctor);
        il.Emit(OpCodes.Ret);

        _dynamicMethodActivator = (Func<object>)createHeadersMethod.CreateDelegate(typeof(Func<object>));

        _expression = Expression.Lambda<Func<object>>(Expression.New(HeadersType)).Compile();  
    }


    [Benchmark(Baseline = true)]
    public object Direct() => new Headers();

    [Benchmark]
    public object Reflection() => Ctor.Invoke(null);

    [Benchmark]
    public object ActivatorCreateInstance() => Activator.CreateInstance(HeadersType);

    [Benchmark]
    public object CompiledExpression() => _expression();

    [Benchmark]
    public object ReflectionEmit() => _dynamicMethodActivator();
}

Using C# 9.0's top-level statements, the Program.cs file is as simple as:

using BenchmarkDotNet.Running;

BenchmarkRunner.Run<ConstructorBenchmarks>();

For completeness, the project file is shown below. I decided to test .NET Framework and .NET 5 separately.

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFrameworks>net461;net5.0</TargetFrameworks>
    <LangVersion>9.0</LangVersion>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="BenchmarkDotNet" Version="0.12.1" />
    <PackageReference Include="Confluent.Kafka" Version="1.6.3" />
  </ItemGroup>

</Project>

To run the benchmarks I used dotnet run -c Release --framework net5.0 for example.

Finally…1500 words later, what are the results!?

The results

Obviously, don't get too hung up on the real numbers here. I'm using a middle-of-the-road laptop from several years ago, but the absolute numbers are not what I'm interested in. I'm more interested in the relative performance of each approach.

First off, lets look at the .NET 5 numbers:

BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19042
Intel Core i7-7500U CPU 2.70GHz (Kaby Lake), 1 CPU, 4 logical and 2 physical cores
.NET Core SDK=5.0.104
  [Host]     : .NET Core 5.0.5 (CoreCLR 5.0.521.16609, CoreFX 5.0.521.16609), X64 RyuJIT
  DefaultJob : .NET Core 5.0.5 (CoreCLR 5.0.521.16609, CoreFX 5.0.521.16609), X64 RyuJIT
MethodMeanErrorStdDevRatioRatioSD
Direct9.851 ns0.1219 ns0.1081 ns1.000.00
Reflection97.489 ns0.7870 ns0.6977 ns9.900.12
ActivatorCreateInstance38.092 ns0.3416 ns0.2853 ns3.870.06
CompiledExpression11.579 ns0.1674 ns0.1398 ns1.180.02
ReflectionEmit12.464 ns0.2388 ns0.2117 ns1.270.02

And the .NET Framework results:

BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19042
Intel Core i7-7500U CPU 2.70GHz (Kaby Lake), 1 CPU, 4 logical and 2 physical cores
  [Host]     : .NET Framework 4.8 (4.8.4300.0), X64 RyuJIT
  DefaultJob : .NET Framework 4.8 (4.8.4300.0), X64 RyuJIT
MethodMeanErrorStdDevRatioRatioSD
Direct12.94 ns0.133 ns0.124 ns1.000.00
Reflection149.51 ns1.027 ns1.142 ns11.580.14
ActivatorCreateInstance66.65 ns1.210 ns1.073 ns5.150.07
CompiledExpression21.60 ns0.481 ns0.515 ns1.670.05
ReflectionEmit15.30 ns0.381 ns0.391 ns1.180.04

Running the benchmark several times, there's a fair amount of variation in the numbers. Being a laptop, I'd imagine it's possible there was some thermal-throttling at play but the general pattern seems quite stable:

  • Standard reflection using ConstructorInfo.Invoke() is roughly 10× slower than calling new Headers()
  • Activator.CreateInstance is 2× faster, i.e. roughly 5× slower than calling new Headers()
  • The compiled expression and dynamic method approaches are roughly the same as calling new Headers(). For .NET 5, we're talking a couple of nanoseconds difference. That's very impressive!
  • On .NET Framework the DynamicMethod approach is faster than using compiled expressions, while on .NET 5, compiled expressions appear to be slightly faster than using DynamicMethod

Given these differences, it seems clear that for performance-sensitive applications, it may well be worth the effort of using compiled expressions or DynamicMethod, even for this simple case.

For completeness, I decided to also benchmark the startup time for each approach. This is a one-off cost that you pay when first calling a specific dynamic method, but it seems like information worth having. This post is already pretty long, so I'll leave out the code for now, but these are the results I get for .NET 5 when the setup costs are included:

BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19042
Intel Core i7-7500U CPU 2.70GHz (Kaby Lake), 1 CPU, 4 logical and 2 physical cores
.NET Core SDK=5.0.104
  [Host]     : .NET Core 5.0.5 (CoreCLR 5.0.521.16609, CoreFX 5.0.521.16609), X64 RyuJIT
  Job-ONVXJR : .NET Core 5.0.5 (CoreCLR 5.0.521.16609, CoreFX 5.0.521.16609), X64 RyuJIT

LaunchCount=50  RunStrategy=ColdStart  
MethodMeanErrorStdDevMedian
Reflection310.29 ns10.195 ns218.95 ns288.30 ns
ActivatorCreateInstance146.38 ns9.178 ns197.10 ns119.70 ns
CompiledExpression90,413.14 ns437.343 ns9,221.75 ns88,071.55 ns
ReflectionEmit86,897.24 ns820.281 ns16,968.07 ns79,778.60 ns

As you can see, compiled expressions and Reflection.Emit have a considerable setup cost. This is important to bear in mind: you'll only start to see the benefits of Reflection.Emit's speedup over using Activator.CreateInstance() after you've called the delegate about 3,500 times! Depending on where you're using this code, that trade off may or may not be worth it!

Summary

In this post I showed 4 different ways to call the constructor of a type using reflection. I then ran benchmarks comparing each approach using BenchmarkDotNet. This shows that using the naïve approach to reflection is about 10× slower than calling the constructor directly, and that using Activator.CreateInstance() is about 5× slower. Both compiled expressions and DynamicMethod with Reflection.Emit approach the speed of calling new Headers() directly, with compiled expressions slightly faster in .NET 5 and ReflectionEmit faster on .NET Framework.

Andrew Lock | .Net Escapades
Want an email when
there's new posts?