简介
我们正在尝试使用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
?首先结合BenchmarksDotNet
和NUnit
来寻找内存泄漏是个好主意吗?
答案 0 :(得分:6)
我是为BenchmarkDotNet实施MemoryDiagnoser的人,我很乐意回答这个问题。
但首先我要描述MemoryDiagnoser的工作原理。
.WithInvocationCount(16)
) 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。它很可能是您案例中最好的工具。