Inspecting aspects and interception in .NET, part 3

Inspecting aspects and interception in .NET, part 3

Previously in this series, I looked at ways we can instrument our code with two of the most basic metrics; how often does our code run and how long does it take to execute? After settling on interception, we took a small detour and looked at how hard it is to measure the time it takes to measure how long it takes to execute your code. This time, we’re going to combine all of them and throw another component in the mix: DI frameworks.

DI, abridged

Most of you probably already know what DI stands for and what dependency injection is. Just a small recap, which is also relevant for the article.

Dependency Injection is about decoupling a class from its dependencies. Instead of requiring a class to construct its own dependencies, it is passed them at construction time. This promotes readability, testability and, as a result of those, maintainability.

A consequence of using a DI framework is that it’s involved in the creation and destruction of most objects in your application. It needs to know, for each class it constructs, which dependencies it requires and how to construct those classes in turn. If you think about the amount of work potentially involved, you quickly come to a realization: it had better be fast.

Scope

A lot of DI frameworks are more flexible than most yoga instructors. Some, less so. As such, it’s impractical and unfair to compare all of the possibilities, and it would make the results very hard to interpret. So let’s define what we’ll be benchmarking.

  • Resolving (or creating) an instance of Something, which implements ISomething, in such a way that calls to ISomething.Foo are timed, in the most common way for the framework in question;
  • Invoking ISomething.Foo.

What we’ll be measuring is the cost of selectively applying a wrapper or interception and what that wrapper or interceptor adds in invocation time.

Frameworks

Since we can’t test all of the DI frameworks out there, I’ve made a—rather arbitrary—selection, based on, amongst others, what my colleagues and I use and want to use and frameworks I’m familiar with. These are the DI frameworks we’ll look at:

We’ll also (sometimes implicitly) look at a number of different interception frameworks:

The benchmark code is available on GitHub, so you can plug in your framework of choice and see how it does.

Let’s see the code

The code is pretty standard; a single method for each combination of frameworks we want to benchmark, decorated with a [Benchmark] attribute. Let’s see what it would normally look like.

[Benchmark]
void AutofacDecorator()
{
    var builder = new ContainerBuilder();

    builder.RegisterType<Something>()
           .Named<ISomething>("inner");
    builder.RegisterDecorator<ISomething>(
        (ctx, inner) => new TimingSomething(inner), 
        fromKey:"inner");

    using (var container = builder.Build())
    {
        var something = container.Resolve<ISomething>();

        something.Foo();
    }
}

This is the entire pipeline, from registering the class and its decorator, to building the container and actually resolving and invoking.

The problem is that registering dependencies can be a pretty expensive operation, but because it’s something you almost always only do once at the start of your application, it’s not very interesting to benchmark.

Amortizing initialization cost

That’s a header I didn’t think I’d ever write in my free time. One approach to separating these costs is to use BenchmarkDotNet’s [Setup] attribute, which exists specifically for this purpose.

We’ll do container construction and registrations there, which mimics real use—because you wouldn’t build up a new container for each request, right? (Right?!)

IContainer _container;

[Setup]
void Initialize()
{
    var builder = new ContainerBuilder();

    builder.RegisterType<Something>()
           .Named<ISomething>("inner");
    builder.RegisterDecorator<ISomething>(
        (c, inner) => new TimingSomething(inner), 
        fromKey:"inner");

    _container = builder.Build();
}

[Benchmark]
void AutofacDecorator()
{
    var something = container.Resolve<ISomething>();

    something.Foo();
}

The Initialize method gets called only once during test initialization, and the only thing that is actually benchmarked is actually resolving and invoking our Something.

Interception

Our interceptor looks pretty much the same for every framework. It has some optimizations, working under the assumption that we can, one-time, gather a list of all the methods that need to be intercepted. For the sake of simplicity, that initialization has been dumbed down to specifically getting the IFoo.Bar method.

internal class DynamicProxyInterceptor : IInterceptor
{
    private static readonly HashSet<MethodBase> _matchingMethods;

    static DynamicProxyInterceptor()
    {
        _matchingMethods = 
            new HashSet<MethodBase>(new[] {GetMatchingMethod()});
    }

    public void Intercept(IInvocation invocation)
    {
        if(!ShouldInterceptMethod(
            invocation.MethodInvocationTarget))
        {
            invocation.Proceed();
            return;
        }

        var stopwatch = Stopwatch.StartNew();

        invocation.Proceed();

        stopwatch.Stop();
    }

    private static bool ShouldInterceptMethod(MethodBase method)
    {
        return _matchingMethods.Contains(method);
    }

    private static MethodBase GetMatchingMethod()
    {
        var type = typeof(Something);
        var interfaceMap = type.GetInterfaceMap(typeof(ISomething));

        int index = Array.FindIndex(
            interfaceMap.InterfaceMethods, 
            method => method.Name == 
                      nameof(ISomething.Foo));

        return interfaceMap.TargetMethods[index];
    }
}

Benchmark results

I won’t go into the code in full detail, that’s for your own enjoyment. So without further ado, let’s see the results of our benchmark.

Method Median StdDev Scaled Place
NoDIDecorator 56,0259 ns 1,0236 ns 1,00 1
SimpleInjectorDecorator 122,5690 ns 2,0028 ns 2,19 2
UnityDecorator 1 113,9534 ns 14,4703 ns 19,88 3
AutofacDecorator 1 190,7673 ns 37,3490 ns 21,25 4
NoDILinFu 1 198,5308 ns 25,1840 ns 21,39 4
NoDIDynamicProxy 4 409,6544 ns 53,3764 ns 78,71 5
SimpleInjectorDynamicProxy 4 541,2929 ns 84,6027 ns 81,06 5
NinjectDecorator 14 394,0852 ns 494,8733 ns 256,92 6
AutofacDynamicProxy 15 736,7079 ns 453,6272 ns 280,88 6
NinjectLinFu 16 606,6503 ns 455,9744 ns 296,41 6
NinjectDynamicProxy 21 277,6210 ns 568,0670 ns 379,78 7
UnityUnityInterception 89 830,2951 ns 1 618,7046 ns 1 603,37 8

Some notes on these results:

  • Methods starting with NoDI are not using any DI framework, as a baseline.
  • If you want to see the specific versions of frameworks I’m using, see the project’s packages.config.
  • Unity (natively) only supports its own interception framework, and that framework is—as far as I can tell—not usable outside of the context of Unity, so that’s why Unity is not combined with Castle DynamicProxy or LinFu and why none of the other frameworks are combined with Unity interception.
  • LinFu is only supported (for automatic interception) by Ninject; that’s why it made no sense to include Autofac + LinFu or SimpleInjector + LinFu, for example.

Observations

Unity interception and Ninject are both so slow compared to the competition, they immediately seem uninteresting. Ninject without any interception is barely faster than Autofac with DynamicProxy. As for Unity interception, it takes around 75 times longer to intercept a method call using Unity interception than it does with LinFu.

SimpleInjector is on the other end of the spectrum; its scaled performance compared to the baseline (remember, that’s not using any DI) is still in the single digits, which is impressive!

Going back to LinFu, look at how fast it is! Kudos to its developers; LinFu stand-alone is even faster (admittedly, by a small margin) than Autofac without interception. It’s so fast that if you integrate it with a slow DI framework, it’s almost like getting interception for free.

Other observations:

  • Autofac and Unity are ‘meh’ in terms of performance; they don’t excel but are not excessively slow, either.
  • None of the DI frameworks add a lot of overhead of their own when using an interception framework.

Conclusion

First up the most obvious conclusion: YMMV. This is by no means a very realistic benchmark. You might encounter that certain frameworks perform a lot worse when they’re loaded down with thousands of registrations, or a lot better when you don’t use the most simple registration possible.

On the other hand, for most applications, time spent in resolving objects is not going to be the bottleneck, so don’t expect it to solve all of your problems. If you want to know if it will, use a profiler and measure first.

That being said, I still think it’s problematic if a DI framework, which is by definition involved in a large part of your application, is ‘slow’. For that reason, I wouldn’t use Unity interception. I would also steer away from Ninject in new projects, unless there is real good reason to use it. Other than that, everything else we tested flies below the radar.

Ease of use

I just want to touch on an aspect which is almost perpendicular to all we’ve been talking about, and that is ease of use. Some integrations between DI frameworks and interception frameworks are easy to use; they require little code and are easy to read and maintain. Others, not so much.

Let’s look at two examples that I find are at opposite ends of the spectrum. First up, Unity.

_container.AddNewExtension<Interception>();

_container.Configure<Interception>()
          .AddPolicy("Metering")
          .AddMatchingRule(new SingleMethodMatchingRule())
          .AddCallHandler(new UnityCallHandler());

This is the piece of code you configure once for your container, which says ‘I want to intercept some services with Unity​Call​Handler’.

_container.RegisterType<ISomething, Something>(
    new Interceptor<InterfaceInterceptor>(),
    new InterceptionBehavior<PolicyInjectionBehavior>()
);

This is a single registration. The Interceptor and Interception​Behavior will have to be repeated for each registration. That is a lot of code to simply say ‘I want to intercept calls to ISomething’.

Now let’s look at SimpleInjector.

_container.InterceptWith<DynamicProxyInterceptor>(
    type => type == typeof(Something)
);

Again, the one-time setup. Note how much simpler it is.

_container.Register<ISomething, Something>(Lifestyle.Transient);

This is the registration of a service. Where’s the part about intercepting? It’s not there, which means you can truly ‘plug in’ interception using the single call to Intercept​With. This might be worth a lot more than performance (although SimpleInjector has little to complain about).

Outcome

I started this series of posts with the premise that I was tasked with adding instrumentation to a project. Given that the project used Unity, my findings were another nail in Unity’s coffin. Long story short, we’re now looking at retro-fitting that project and are using SimpleInjector for future projects.

Oh, and if anybody is integrating LinFu with SimpleInjector, I’d love to know. Drop me a line!