12
votes

In this article (11 Apr 2009), the author claims Hiro is:

"the World's Fastest IOC Container... a statically precompiled IOC container that performs as fast as an application without an IOC container".

Is it still the fastest IOC container today? Is it ready for production? Are there any other containers can do IOC at compile time? What are its major advantages and disadvantages over other IOC containers?

Thanks

1
do you really need absolute top speed from a IoC container, which in 99% of apps is not the bottleneck ever?Mauricio Scheffer
The Simple Service Locator claims to be very fast as well: simpleservicelocator.codeplex.com/discussions/236029. I bet it will outperform Hiro.Steven
@Mauricio Scheffer, Yes, I do :) in my current project, I need to create about 10,000+ objects per 0.1 second. With Ninject, this is impossible (Ninject can only create 2,000 objects per 0.1 second on my machine). I'm currently using Autofac, which is 4 times faster than Ninject. I'm OK with its speed (for my currently project). But my next project might need to handle more objects.user593358
@caveman: I'd reconsider doing that, I think your app would spend some non-trivial % of time in GC.Mauricio Scheffer
@caveman the component you're creating 10,000 times per second - if it is such a hot path, why not just fall back to using "new"? Usually optimisation means using the cleanest tools/techniques/patterns for 99% of the code, then whatever's quickest for that hot 1%...Nicholas Blumhardt

1 Answers

20
votes

Hiro claims to be the fastest container. This statement is based on the benchmark given by the author (see here for an objective comparison between many containers). Whether or not this benchmark is realistic depends upon the size of your application. The benchmark seems to be cleverly setup with a very small set of registered types. When adding more registrations to the benchmark, the performance starts dropping (see below for an example benchmark of this). When looking closely we can see that Hiro has a performance characteristic of O(n), while normal DI frameworks have a characteristic of O(1), thus with other frameworks the performance stays constant with the number of registered types.

What’s nice about Hiro is that it generates a new assembly on the fly and resolving a new type consists of just a single interface call. This is very fast. Most common DI frameworks on the other hand, will always have to do a dictionary lookup during a call to GetInstance. Hiro’s GetInstance method is basically a big switch case statement internally, implemented as a bunch of if-statements. If-based switch case statements are extremely fast up until a point. The C# compiler uses an heuristic of (I believe) 18 case statements as turning point. Below that number the compiler generates a bunch of if-statements. Above that number it creates and stores a static dictionary and does a dictionary lookup, because above 18 lookups performance of a dictionary lookup if simply faster.

Well guess what; Hiro doesn’t use the optimization as the C# compiler does. That's why the performance keeps dropping as more and more types are added to the container. A linear performance characteristic –or O(n) for short- is okay for small sets of data, but any well written and commonly sized, dependency-friendly application has many type registrations / mappings. And in that case the performance of Hiro drops quickly.

So, to answer your questions:

Is it still the fastest IOC container today?

For very small applications it is the fastest. For any normal sized applications it has never been the fastest.

Is it ready for production?

Hard to say. Its current status is alpha and it misses a lot of features that other IOC framework have. The developer just (see comments below) published a stable release. This means that according to the developer it is ready for production.

What are its major advantages and disadvantages over other IOC containers?

Take a look for instance at this article (and the follow up) which gives a good (feature) comparison of IOC frameworks. The writer of the article has a vision of what he thinks is important. You have to make such a list for yourself. Performance seems to be high on your list. However, please note that there are other ways to improve performance. For instance by registering types as singletons to prevent the creation of many types.

Here is a little comparison with other containers. Note that I didn't write Hiro, so I could be missing things, especially since there seems to be no documentation at all, but here goes:

  1. You need 4 assemblies to run, instead of 1 or 2 for most IOC frameworks.
  2. It throws a stack overflow when a recursive dependency is resolved (most frameworks do this btw). So there is no cyclic dependency detection.
  3. Doesn't seem to support any lifestyle other than transient (the writer does say it will, but I currently see no support for this). This actually is bad for performance, because most services would normally be registered as singletons. According to the author, it supports multiple lifestyles.
  4. Doesn't support resolving open generic types.
  5. Doesn't support unregistered type resolution.
  6. I has no documentation accept XML (intellisense) documentation.

Are there any other containers can do IOC at compile time?

Define 'compile time'. Hiro generates a new assembly on the fly once during runtime. Others (like Autofac, Windsor and Simple Injector) emit IL or compile delegates under the covers. This isn't any slower than compiling the complete assembly in one go (however, there is the overhead of calling the delegate which is a tiny bit slower than making an interface call).

BTW, I changed the benchmark to see the behavior described above appear. With 50 extra types registered you will see that Hiro performs already three times as slow as with the initial benchmark. Here is the code I used:

public interface IHandler<T> { }

public class Handler<T> : IHandler<T> { }

public class HiroUseCase : UseCase
{
    IMicroContainer container;

    private static void RegisterHandler<T>(DependencyMap map)
    {
        map.AddService(typeof(IHandler<T>), typeof(Handler<T>));
    }       

    public HiroUseCase()
    {
        var map = new DependencyMap();

        // *** My added registrations
        RegisterHandler<byte>(map);
        RegisterHandler<byte?>(map);
        RegisterHandler<short>(map);
        RegisterHandler<short?>(map);
        RegisterHandler<ushort>(map);
        RegisterHandler<ushort?>(map);
        RegisterHandler<int>(map);
        RegisterHandler<int?>(map);
        RegisterHandler<uint>(map);
        RegisterHandler<uint?>(map);

        RegisterHandler<long>(map);
        RegisterHandler<long?>(map);
        RegisterHandler<ulong>(map);
        RegisterHandler<ulong?>(map);
        RegisterHandler<float>(map);
        RegisterHandler<float?>(map);
        RegisterHandler<double>(map);
        RegisterHandler<double?>(map);
        RegisterHandler<decimal>(map);
        RegisterHandler<decimal?>(map);

        RegisterHandler<DateTime>(map);
        RegisterHandler<DateTime?>(map);
        RegisterHandler<char>(map);
        RegisterHandler<char?>(map);
        RegisterHandler<object>(map);
        RegisterHandler<string>(map);
        RegisterHandler<bool>(map);
        RegisterHandler<bool?>(map);
        RegisterHandler<Enum>(map);
        RegisterHandler<DateTimeKind>(map);

        RegisterHandler<DateTimeKind?>(map);
        RegisterHandler<DateTimeOffset>(map);
        RegisterHandler<DateTimeOffset?>(map);
        RegisterHandler<DayOfWeek>(map);
        RegisterHandler<DayOfWeek?>(map);
        RegisterHandler<DBNull>(map);
        RegisterHandler<Delegate>(map);
        RegisterHandler<DivideByZeroException>(map);
        RegisterHandler<DllNotFoundException>(map);
        RegisterHandler<Exception>(map);

        RegisterHandler<KeyNotFoundException>(map);
        RegisterHandler<InvalidOperationException>(map);
        RegisterHandler<InvalidCastException>(map);
        RegisterHandler<InvalidProgramException>(map);
        RegisterHandler<InvalidTimeZoneException>(map);
        RegisterHandler<IDisposable>(map);
        RegisterHandler<IComparable>(map);
        RegisterHandler<IEquatable<int>>(map);
        RegisterHandler<IEnumerable>(map);
        RegisterHandler<IEqualityComparer>(map);

        // *** Original benchmark setup
        map.AddService(typeof(IWebApp), typeof(WebApp));
        map.AddService(typeof(IAuthenticator), typeof(Authenticator));
        map.AddService(typeof(IStockQuote), typeof(StockQuote));
        map.AddService(typeof(IDatabase), typeof(Database));
        map.AddService(typeof(IErrorHandler), typeof(ErrorHandler));
        map.AddService(typeof(ILogger), typeof(Logger));

        IContainerCompiler compiler = new ContainerCompiler();
        var assembly = compiler.Compile(map);;

        var loadedAssembly = assembly.ToAssembly();
        var containerType = loadedAssembly.GetTypes()[0];
        container = (IMicroContainer)Activator
            .CreateInstance(containerType);
    }

    public override void Run()
    {
        var webApp = 
            (IWebApp)container.GetInstance(typeof(IWebApp), null);
        webApp.Run();
    }
}