【问题标题】:DI containers leak memory or BenchmarksDotNet MemoryDiagnoser delivers inaccurate measurements?DI 容器泄漏内存或 BenchmarksDotNet MemoryDiagnoser 提供不准确的测量?
【发布时间】:2018-08-06 02:50:02
【问题描述】:

简介

我们正在尝试使用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 在这里意味着没有内存泄漏,而不是因为编译器优化没有运行代码。

问题

当我们尝试用TinyIoC 容器解析TestClass 时,混乱就开始了:

[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 值与来自TestBenchmark1 的ResolveTestClass 调用次数成比例增加,

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

顺便说一句,用Autofac 依赖注入框架替换TinyIoC 并没有太大改变这种情况。

问题

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

【问题讨论】:

    标签: memory-leaks autofac tinyioc benchmarkdotnet


    【解决方案1】:

    我是为 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分配量子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。我个人usedVisual Studio Memory Profiler,它是 Visual Studio 的一部分。

    另一种选择是使用JetBrains.DotMemoryUnit。它很可能是您的最佳工具。

    【讨论】:

    • 亲爱的亚当,我很高兴看到MemoryDiagnoser 创作者的回答!非常感谢你们的工作,你们所做的真是太棒了!
    • 我尝试在BenchmarksConfig 中用1024 替换16,测试输出报告了与ResolveTestClass 相同的实验数字,但它也报告了64 B 与CreateTestClass 的实验。我还尝试完全删除Add(Job.Default .WithLaunchCount(1) .WithTargetCount(1) .WithWarmupCount(1) .WithInvocationCount(16)); 行,让BenchmarksDotNet 自行决定迭代次数并获得与1024 调用计数相同的结果:64 B 使用ctor 和1.07 KB 创建TestClass 时从容器中解决它时。
    • @foxanna 那么你需要使用一些内存分析器来验证结果。 1.07 KB 可能是真的
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-09-09
    • 2011-12-13
    • 2012-07-16
    • 2010-09-08
    • 2016-02-04
    相关资源
    最近更新 更多