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

fox*_*nna 4 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} ");
}
Run Code Online (Sandbox Code Playgroud)

我们正在通过NUnit测试实现基准测试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();
    }
}
Run Code Online (Sandbox Code Playgroud)

测试输出包含以下摘要:

         Method | Mean | Error | Allocated |
--------------- |-----:|------:|----------:|
 TestBenchmark1 |   NA |    NA |       0 B |
Run Code Online (Sandbox Code Playgroud)

测试输出还包含所有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();
    }
}
Run Code Online (Sandbox Code Playgroud)

摘要表明泄漏了1.07 KB.

         Method | Mean | Error | Allocated |
--------------- |-----:|------:|----------:|
 TestBenchmark1 |   NA |    NA |   1.07 KB |
Run Code Online (Sandbox Code Playgroud)

Allocated比例值增加的要数ResolveTestClass从通话TestBenchmark1中,摘要

[Benchmark]
public void TestBenchmark1() 
{
    ResolveTestClass("Test");
    ResolveTestClass("Test");
}
Run Code Online (Sandbox Code Playgroud)

         Method | Mean | Error | Allocated |
--------------- |-----:|------:|----------:|
 TestBenchmark1 |   NA |    NA |   2.14 KB |
Run Code Online (Sandbox Code Playgroud)

这表明要么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);
    }
}
Run Code Online (Sandbox Code Playgroud)

顺便提一下,更换TinyIoCAutofac依赖注入框架没有改变的情况很多.

问题

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

Ada*_*nik 6

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

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

  1. 它通过使用可用的API获取分配的内存数量.
  2. 它执行一次额外的基准测试迭代.在你的情况下,它是16(.WithInvocationCount(16))
  3. 它通过使用可用的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);
Run Code Online (Sandbox Code Playgroud)

可能最终报告:

x
x + 8000
Run Code Online (Sandbox Code Playgroud)

BenchmarkDotNet如何处理这个问题?我们执行大量调用(通常是数百万或数十亿),因此最小化分配量子大小问题(对于我们来说,它永远不会是8k).

如何解决您的问题:将WithInvocationCount数字设置为更大的数字(可能是1000).

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

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

  • 我尝试在`BenchmarksConfig`中用`1024`替换'16`并测试输出报告与`ResolveTestClass`的体验相同的数字,但它也报告了`64 B`用于实验`CreateTestClass`.我也尝试删除`添加(Job.Default .WithLaunchCount(1).WithTargetCount(1).WithWarmupCount(1).WithInvocationCount(16));`行让`BenchmarksDotNet`自行决定迭代次数并获得与`1024`调用计数相同的结果:`64 B`在使用`ctor`创建`TestClass`和从容器中解析时为`1.07 KB`. (2认同)