Does DI container leak memory or BenchmarksDotNet MemoryDiagnoser provide inaccurate measurements?

Introduction

We are trying to catch potential memory leaks with BenchmarksDotNet.

For simplicity, the example here is inexperienced TestClass:

public class TestClass 
{
    private readonly string _eventName;

    public TestClass(string eventName)
    {
        _eventName = eventName;
    }

    public void TestMethod() =>
        Console.Write($@"{_eventName} ");
}

We do benchmarking, though NUnit's tests are netcoreapp2.0:

[TestFixture]
[MemoryDiagnoser]
public class TestBenchmarks
{
    [Test]
    public void RunTestBenchmarks() =>
        BenchmarkRunner.Run<TestBenchmarks>(new BenchmarksConfig());

    [Benchmark]
    public void TestBenchmark1() =>
        CreateTestClass("Test");

    private void CreateTestClass(string eventName)
    {
        var testClass = new TestClass(eventName);
        testClass.TestMethod();
    }
}

The test output contains the following summary:

         Method | Mean | Error | Allocated |
--------------- |-----:|------:|----------:|
 TestBenchmark1 |   NA |    NA |       0 B |

The test output also contains all the output Console.Write, which proves that 0 Bhere it means that the memory was not skipped and the code was not run due to compiler optimization.

Problem

The confusion begins when we try to resolve TestClasswith the container TinyIoC:

[TestFixture]
[MemoryDiagnoser]
public class TestBenchmarks
{
    private TinyIoCContainer _container;

    [GlobalSetup]
    public void SetUp() =>
        _container = TinyIoCContainer.Current;

    [Test]
    public void RunTestBenchmarks() =>
        BenchmarkRunner.Run<TestBenchmarks>(new BenchmarksConfig());

    [Benchmark]
    public void TestBenchmark1() => 
        ResolveTestClass("Test");

    private void ResolveTestClass(string eventName)
    {
        var testClass = _container.Resolve<TestClass>(
            NamedParameterOverloads.FromIDictionary(
                new Dictionary<string, object> {["eventName"] = eventName}));
        testClass.TestMethod();
    }
}

The bulletin indicates a leak of 1.07 KB.

         Method | Mean | Error | Allocated |
--------------- |-----:|------:|----------:|
 TestBenchmark1 |   NA |    NA |   1.07 KB |

Allocatedthe value increases in proportion to the number of calls ResolveTestClassfrom TestBenchmark1, a summary for

[Benchmark]
public void TestBenchmark1() 
{
    ResolveTestClass("Test");
    ResolveTestClass("Test");
}

is an

         Method | Mean | Error | Allocated |
--------------- |-----:|------:|----------:|
 TestBenchmark1 |   NA |    NA |   2.14 KB |

, TinyIoC ( ), BenchmarksDotNet , [Benchmark].

, :

public class BenchmarksConfig : ManualConfig
{
    public BenchmarksConfig()
    {
        Add(JitOptimizationsValidator.DontFailOnError); 

        Add(DefaultConfig.Instance.GetLoggers().ToArray()); 
        Add(DefaultConfig.Instance.GetColumnProviders().ToArray()); 

        Add(Job.Default
            .WithLaunchCount(1)
            .WithTargetCount(1)
            .WithWarmupCount(1)
            .WithInvocationCount(16));

        Add(MemoryDiagnoser.Default);
    }
}

, TinyIoC Autofac .

, DI - ? , BenchmarksDotNet ? NUnit BenchmarksDotNet ?

+4
1

, MemoryDiagnoser BenchmarkDotNet, .

, MemoryDiagnoser.

  • , API.
  • . 16 (.WithInvocationCount(16))
  • , API.

final result = (totalMemoryAfter - totalMemoryBefore) / invocationCount

? , API-, : GC.GetAllocatedBytesForCurrentThread() .NET Core 1.1+ AppDomain.MonitoringTotalAllocatedMemorySize .NET 4.6 +.

, GC Allocation Quantum . 8k .

: new object(), GC ( ), 8k . API 8k , .

Console.WriteLine(AppDomain.MonitoringTotalAllocatedMemorySize);
GC.KeepAlive(new object());
Console.WriteLine(AppDomain.MonitoringTotalAllocatedMemorySize);

:

x
x + 8000

BenchmarkDotNet ? ( ), ( 8k).

: WithInvocationCount (, 1000).

, . Visual Studio Memory Profiler, Visual Studio.

JetBrains.DotMemoryUnit. , , .

+6

Source: https://habr.com/ru/post/1694137/


All Articles