DevTeam / Pure.DI

Pure DI for .NET
MIT License
410 stars 21 forks source link

Resolving transient dependencies with MS DI is really slow #13

Closed MisinformedDNA closed 1 year ago

MisinformedDNA commented 1 year ago

If I use Pure.DI to inject and resolve dependencies, then it is really quick. If I use MS DI to inject and resolve dependencies, then it is faster. If I use inject with Pure.DI and resolve with MS DI, it is really slow.

Method Mean Error StdDev Ratio RatioSD
PureDI 5.278 ns 0.1770 ns 0.2857 ns 1.00 0.00
DotNet 45.698 ns 0.9411 ns 1.0837 ns 8.67 0.58
PureDIResolvedWithMS 71.330 ns 0.9999 ns 0.9353 ns 13.60 0.86

I would have expected injecting with Pure.DI and resolving with MS DI to be somewhere in the middle, not significantly worse. Do you have any Ideas on what is happening?

Code:

using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using Microsoft.Extensions.DependencyInjection;
using Pure.DI;

var summary = BenchmarkRunner.Run<DIComparison>();

public class DIComparison
{
    private IServiceProvider _microsoftServiceProvider;
    private IServiceProvider _pureDIAndMSServiceProvider;
    private IServiceProvider _handCraftedServiceProvider;

    [GlobalSetup]
    public void Setup()
    {
        _microsoftServiceProvider = new ServiceCollection()
            .AddTransient<Something>()
            .AddTransient<SomethingElse>()
            .BuildServiceProvider();

        _pureDIAndMSServiceProvider = new ServiceCollection()
            .AddPureDIClass()
            .BuildServiceProvider();
    }

    [Benchmark(Baseline = true)]
    public Something PureDI() => PureDIClass.Resolve<Something>();

    [Benchmark(Baseline = false)]
    public Something DotNet() => _microsoftServiceProvider.GetRequiredService<Something>();

    [Benchmark]
    public Something PureDIResolvedWithMS() => _pureDIAndMSServiceProvider.GetRequiredService<Something>();
}

public class Something 
{
    public Something(SomethingElse somethingElse) { }
}

public class SomethingElse { }

public static partial class PureDIClass
{
    public static void Setup() => DI.Setup()
        .Bind<Something>().As(Lifetime.Transient).To<Something>()
        .Bind<SomethingElse>().As(Lifetime.Transient).To<SomethingElse>();
}
NikolayPianikov commented 1 year ago

I will make a research

NikolayPianikov commented 1 year ago

Please add another one test like:

[Benchmark]
public Something FastPureDI() => PureDIClass.ResolveSomething();

Just for the big picture :)

MisinformedDNA commented 1 year ago
Method Mean Error StdDev Ratio RatioSD
PureDI_ResolveType 4.561 ns 0.1527 ns 0.1634 ns 1.00 0.00
PureDI_DirectMethod 2.370 ns 0.0657 ns 0.0583 ns 0.52 0.03
MS_ResolveType 42.272 ns 0.2464 ns 0.2184 ns 9.23 0.36
MS_ImplementationFactory 55.598 ns 0.2616 ns 0.2447 ns 12.17 0.45
PureDI_ResolvedWithMS 66.791 ns 0.7181 ns 0.6717 ns 14.62 0.63
CommunityToolkit 30.811 ns 0.2624 ns 0.2454 ns 6.74 0.23
PureDI_ResolvedWithCommunityToolkit 53.565 ns 0.5580 ns 0.5219 ns 11.72 0.49
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using Microsoft.Extensions.DependencyInjection;
using Pure.DI;
using CommunityToolkit.Mvvm.DependencyInjection;

var summary = BenchmarkRunner.Run<DIComparison>();

//[ShortRunJob]
public class DIComparison
{
    private IServiceProvider _microsoftServiceProvider;
    private IServiceProvider _pureDIServiceProvider;
    private IServiceProvider _handCraftedServiceProvider;

    [GlobalSetup]
    public void Setup()
    {
        _microsoftServiceProvider = new ServiceCollection()
            .AddTransient<Something>()
            .AddTransient<SomethingElse>()
            .BuildServiceProvider();

        _pureDIServiceProvider = new ServiceCollection()
            .AddPureDIClass()
            .BuildServiceProvider();

        Ioc.Default.ConfigureServices(_microsoftServiceProvider);

        _handCraftedServiceProvider = new ServiceCollection()
            .AddTransient<Something>(sp => new Something(new SomethingElse()))
            .AddTransient<SomethingElse>(sp => new SomethingElse())
            .BuildServiceProvider();
    }

    [GlobalSetup(Target = nameof(PureDI_ResolvedWithCommunityToolkit))]
    public void Setup2()
    {
        _pureDIServiceProvider = new ServiceCollection()
            .AddPureDIClass()
            .BuildServiceProvider();

        Ioc.Default.ConfigureServices(_pureDIServiceProvider);
    }

    [Benchmark(Baseline = true)]
    public Something PureDI_ResolveType() => PureDIClass.Resolve<Something>();

    [Benchmark]
    public Something PureDI_DirectMethod() => PureDIClass.ResolveSomething();

    [Benchmark]
    public Something MS_ResolveType() => _microsoftServiceProvider.GetRequiredService<Something>();

    [Benchmark]
    public Something MS_ImplementationFactory() => _handCraftedServiceProvider.GetRequiredService<Something>();

    [Benchmark]
    public Something PureDI_ResolvedWithMS() => _pureDIServiceProvider.GetRequiredService<Something>();

    [Benchmark]
    public Something CommunityToolkit() => Ioc.Default.GetRequiredService<Something>();

    [Benchmark]
    public Something PureDI_ResolvedWithCommunityToolkit() => Ioc.Default.GetRequiredService<Something>();
}

public class Something
{
    public Something(SomethingElse somethingElse) { }
}

public class SomethingElse { }

public static partial class PureDIClass
{
    public static void Setup() => DI.Setup()
        .Bind<Something>().As(Lifetime.Transient).To<Something>()
        .Bind<SomethingElse>().As(Lifetime.Transient).To<SomethingElse>();
}
NikolayPianikov commented 1 year ago

@MisinformedDNA thank you, I am going to add this benchmarks to Pure.DI.Benchmark. Any objections?

NikolayPianikov commented 1 year ago

By the way, you can leave out the Transient because of it is the default lifetime. Also you can override the default lifetime by the call like Default(Lifetime.Singleton).

Also you might skip adding binding for SomethingElse because of it is not abstract type or interface and can be created by constructor. And it is not a composition root type. So a binding is not required there and this dependency will be automatically injected into Something.

Thus this code:

public static partial class PureDIClass
{
    public static void Setup() => DI.Setup()
        .Bind<Something>().To<Something>();
}

should work too.

Please see the following samples:

MisinformedDNA commented 1 year ago

@MisinformedDNA thank you, I am going to add this benchmarks to Pure.DI.Benchmark. Any objections?

Nope. Sounds good.

NikolayPianikov commented 1 year ago

This issue was fixed and the benchmarks were added. NuGet

MisinformedDNA commented 1 year ago

@NikolayPianikov

Don't you mean "with Service Collection":

_pureDIWithoutServiceCollectionServiceProvider = new Microsoft.Extensions.DependencyInjection.ServiceCollection()
    .AddServiceCollectionDI()
    .BuildServiceProvider();

[Benchmark(Description = "Service Provider without ServiceCollection in Pure.DI")]
    public ICompositionRoot PureDIWithoutServiceCollectionResolve() => 
        _pureDIWithoutServiceCollectionServiceProvider.GetRequiredService<ICompositionRoot>();

If so, the benchmarks you ran still show that using Pure.DI with MS's ServiceCollection makes DI slower.

NikolayPianikov commented 1 year ago

Yes, fixed in the Last commit. Now it works as "crafted" by performance

NikolayPianikov commented 1 year ago

You are right, I am plannig to reserch how the toolkit makes it faster. But actually it create "crafted" now.

NikolayPianikov commented 1 year ago

I reopened this ticket and Will close it after the research

MisinformedDNA commented 1 year ago

What do you mean by "But actually it create "crafted" now."?

MisinformedDNA commented 1 year ago

It seems odd to me, but .AddTransient<Something>(sp => new Something()) is just slower than .AddTransient<Something>()

NikolayPianikov commented 1 year ago

What do you mean by "But actually it create "crafted" now."?

I mean that actual code for "ServiceCollection" looks like:

internal static Microsoft.Extensions.DependencyInjection.IServiceCollection AddServiceCollectionDI(this Microsoft.Extensions.DependencyInjection.IServiceCollection services)
{
        Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddTransient<Pure.DI.Benchmark.Model.ICompositionRoot>(services, _ => new Pure.DI.Benchmark.Model.CompositionRoot(SingletonPureDIBenchmarkModelService1.Shared, new Pure.DI.Benchmark.Model.Service2(new Pure.DI.Benchmark.Model.Service3(), new Pure.DI.Benchmark.Model.Service3(), new Pure.DI.Benchmark.Model.Service3(), new Pure.DI.Benchmark.Model.Service3(), new Pure.DI.Benchmark.Model.Service3()), new Pure.DI.Benchmark.Model.Service2(new Pure.DI.Benchmark.Model.Service3(), new Pure.DI.Benchmark.Model.Service3(), new Pure.DI.Benchmark.Model.Service3(), new Pure.DI.Benchmark.Model.Service3(), new Pure.DI.Benchmark.Model.Service3()), new Pure.DI.Benchmark.Model.Service2(new Pure.DI.Benchmark.Model.Service3(), new Pure.DI.Benchmark.Model.Service3(), new Pure.DI.Benchmark.Model.Service3(), new Pure.DI.Benchmark.Model.Service3(), new Pure.DI.Benchmark.Model.Service3()), new Pure.DI.Benchmark.Model.Service3()));
        Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddTransient<Pure.DI.Benchmark.Model.IService1>(services, _ => SingletonPureDIBenchmarkModelService1.Shared);
        Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddTransient<Pure.DI.Benchmark.Model.IService2>(services, _ => new Pure.DI.Benchmark.Model.Service2(new Pure.DI.Benchmark.Model.Service3(), new Pure.DI.Benchmark.Model.Service3(), new Pure.DI.Benchmark.Model.Service3(), new Pure.DI.Benchmark.Model.Service3(), new Pure.DI.Benchmark.Model.Service3()));
        Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddTransient<Pure.DI.Benchmark.Model.IService3>(services, _ => new Pure.DI.Benchmark.Model.Service3());
        return services;
}
NikolayPianikov commented 1 year ago

It seems odd to me, but .AddTransient(sp => new Something()) is just slower than .AddTransient()

Yes, you are right. But Pure DI cannot use the AddTransient<Something>() approach because the object graph built by the MS library is different from the Pure DI object graph, but Pure DI should work consistently in any scenario. For example, when developers use factories, tags, or generics, or when DI selects a constructor and etc. That is why the lambda approach is used, since only in this case Pure DI can control the construction of the object graph.

MisinformedDNA commented 1 year ago

In terms of transients only, PureDI with ServiceCollection will have the same perf as the crafted code, since they use the same lambdas.

As for community toolkit, it is using the same service provider built by the same service collection, but service resolution is simpler for the toolkit, like not checking if the service provider inherits from ISupportRequiredService. But there must be other differences that I'm not seeing.

NikolayPianikov commented 1 year ago

Yes, I have looked and it looks strange that it is faster than the direct service provider. It actually makes 2 virtual calls:

    IL_0000: call         class [Microsoft.Toolkit.Mvvm]Microsoft.Toolkit.Mvvm.DependencyInjection.Ioc [Microsoft.Toolkit.Mvvm]Microsoft.Toolkit.Mvvm.DependencyInjection.Ioc::get_Default()
    IL_0005: callvirt     instance !!0/*class Pure.DI.Benchmark.Model.ICompositionRoot*/ [Microsoft.Toolkit.Mvvm]Microsoft.Toolkit.Mvvm.DependencyInjection.Ioc::GetRequiredService<class Pure.DI.Benchmark.Model.ICompositionRoot>()
    IL_000a: ret

and

    IL_0010: ldtoken      !!0/*T*/
    IL_0015: call         class [System.Runtime]System.Type [System.Runtime]System.Type::GetTypeFromHandle(valuetype [System.Runtime]System.RuntimeTypeHandle)
    IL_001a: callvirt     instance object [System.ComponentModel]System.IServiceProvider::GetService(class [System.Runtime]System.Type)
    IL_001f: unbox.any    !!0/*T*/
NikolayPianikov commented 1 year ago

It looks like MS extensions do more checks compared to ToolKit because the method GetService(typeof(ICompositionRoot)) without extensions has the same performance:

BenchmarkDotNet=v0.13.2, OS=Windows 11 (10.0.22000.1098/21H2)
Intel Core i7-10875H CPU 2.30GHz, 1 CPU, 16 logical and 8 physical cores
.NET SDK=6.0.301
  [Host]     : .NET 6.0.10 (6.0.1022.47605), X64 RyuJIT AVX2
  DefaultJob : .NET 6.0.10 (6.0.1022.47605), X64 RyuJIT AVX2
Method | Mean | Error | StdDev | Ratio | RatioSD -- | -- | -- | -- | -- | -- 'MS Service Provider' | 24.06 ns | 0.547 ns | 0.899 ns | 1.00 | 0.00 Crafted | 34.66 ns | 0.745 ns | 1.181 ns | 1.44 | 0.06 'Community Toolkit' | 23.12 ns | 0.446 ns | 0.639 ns | 0.96 | 0.05 Pure.DI | 11.85 ns | 0.309 ns | 0.491 ns | 0.49 | 0.02 'Pure.DI with ServiceCollection' | 33.94 ns | 0.731 ns | 1.260 ns | 1.41 | 0.06