Утечка памяти в контейнерах DI или BenchmarksDotNet MemoryDiagnoser обеспечивает неточные измерения?
Вступление
Мы пытаемся уловить потенциальные утечки памяти, используя BenchmarksDotNet
,
Для простоты примера, вот простой TestClass
:
public class TestClass
{
private readonly string _eventName;
public TestClass(string eventName)
{
_eventName = eventName;
}
public void TestMethod() =>
Console.Write($@"{_eventName} ");
}
Мы реализуем бенчмаркинг, хотя тесты 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();
}
}
Результат теста содержит следующую сводку:
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 КБ было утечка.
Method | Mean | Error | Allocated |
--------------- |-----:|------:|----------:|
TestBenchmark1 | NA | NA | 1.07 KB |
Allocated
значение увеличивается пропорционально количеству ResolveTestClass
звонки из 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
с Autofac
структура внедрения зависимостей не сильно изменила ситуацию.
Вопросы
Означает ли это, что вся структура DI должна реализовывать своего рода кеш для разрешенных объектов? Значит ли это BenchmarksDotNet
неправильно используется в данном примере? Это хорошая идея, чтобы охотиться на утечки памяти с помощью комбинации NUnit
а также BenchmarksDotNet
на первом месте?
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, определяет размер выделенной памяти. Обычно это 8 Кбайт.
Что это действительно означает: если мы выделим один объект с new object()
и GC должен выделить память для него (текущий сегмент заполнен), он собирается выделить 8 КБ памяти. И оба API сообщат о выделении 8 КБ памяти после выделения одного объекта.
Console.WriteLine(AppDomain.MonitoringTotalAllocatedMemorySize);
GC.KeepAlive(new object());
Console.WriteLine(AppDomain.MonitoringTotalAllocatedMemorySize);
может в конечном итоге в отчетности:
x
x + 8000
Как BenchmarkDotNet справляется с этой проблемой? Мы выполняем МНОГО вызовов (обычно миллионы или миллиарды), поэтому минимизируем проблему квантового размера распределения (для нас это никогда не 8 КБ).
Как решить проблему в вашем случае: установите WithInvocationCount
на большее число (может быть, 1000).
Чтобы проверить результаты, вы можете рассмотреть возможность использования Memory Profiler. Лично я использовал Visual Studio Memory Profiler, который является частью Visual Studio.
Другой альтернативой является использование JetBrains.DotMemoryUnit. Скорее всего, это лучший инструмент в вашем случае.