DI容器泄漏内存或BenchmarksDotNet MemoryDiagnoser提供不准确的测量?

时间:2018-02-26 15:42:08

标签: memory-leaks autofac tinyioc benchmarkdotnet

简介

我们正在尝试使用BenchmarksDotNet捕获潜在的内存泄漏。

为简单起见,这里有一个简单的TestClass

public class TestClass 
{
    private readonly string _eventName;

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

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

我们正在通过netcoreapp2.0中的NUnit测试实施基准测试:

[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();
    }
}

测试输出包含以下摘要:

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

测试输出还包含所有Console.Write输出,这证明0 B这意味着没有内存泄漏而不是因为编译器优化而没有运行代码。

问题

当我们尝试使用TestClass容器解析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();
    }
}

摘要表明泄露了1.07 KB。

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

Allocated价值与来自ResolveTestClass的{​​{1}}来电的数量成比例增加,

的摘要
TestBenchmark1

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

这表明 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并没有改变这种情况。

问题

这是否意味着所有DI框架都必须为已解析的对象实现某种缓存?是否意味着在给定的示例中以错误的方式使用Autofac?首先结合BenchmarksDotNetNUnit来寻找内存泄漏是个好主意吗?

1 个答案:

答案 0 :(得分:6)

我是为BenchmarkDotNet实施MemoryDiagnoser的人,我很乐意回答这个问题。

但首先我要描述MemoryDiagnoser的工作原理。

  1. 使用可用的API获取已分配内存的数量。
  2. 它执行一次额外的基准运行迭代。在你的情况下,它是16(.WithInvocationCount(16)
  3. 使用可用的API获取已分配内存的数量。
  4. final result = (totalMemoryAfter - totalMemoryBefore) / invocationCount

    结果有多准确?它与我们使用的可用API一样准确:GC.GetAllocatedBytesForCurrentThread()用于.NET Core 1.1+,AppDomain.MonitoringTotalAllocatedMemorySize用于.NET 4.6 +。

    GC分配量子defines称为已分配内存的大小。它通常是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)。

    要验证结果,您可以考虑使用某些Memory Profiler。我个人used Visual Studio Memory Profiler,它是Visual Studio的一部分。

    另一种方法是使用JetBrains.DotMemoryUnit。它很可能是您案例中最好的工具。

相关问题