Как интерпретировать результаты BenchmarkDotNet и dotMemory?

Итак, у меня есть следующий кусок кода в моем Main() метод

for (int x = 0; x < 100; x++) // to mimic BenchmarkDotnet runs
   for (int y = 0; y < 10000; y++)
     LogicUnderTest();

Далее у меня следующий класс под тестом

[MemoryDiagnoser, ShortRunJob]
public class TestBenchmark
{
    [Benchmark]
    public void Test_1()
    {
        for (int i = 0; i < 10000; i++)
            LogicUnderTest();
    }
}

После запуска Main() под dotMemory около 6 минут я получаю следующие результаты

Приложение начинается с 10Mb и идет до 14Mb,

Но когда я бегу BenchmarkDotnet тест я получаю это

Я вижу, что у меня есть 2.6GB выделены. Какие? Кажется, не совсем хорошо. Кроме того, я не вижу Gen1 а также Gen2 колонны. Означает ли это, что код не выделил в них ничего, поэтому отображать нечего?

Как я могу интерпретировать результаты? Кажется, в порядке DotMemory, но не в порядке в BenchmarkDotNet, Я довольно новичок в BenchmarkDotnet и будет полезен для любой информации относительно результатов.

PS. LogicUnderTest() активно работает со строками.

PSS. Грубо говоря, LogicUnderTest реализован так

void LogicUnderTest()
{
    var dict = new Dictionary<int, string>();
    for (int j = 0; j < 1250; j++)
        dict.Add(j, $"index_{j}");
    string.Join(",", dict.Values);
}

3 ответа

Я автор MemoryDiagnoser и я также предоставил ответ на ваш вопрос в моем блоге. Я просто скопирую его здесь:

Как читать результаты

|     Method |  Gen 0 | Allocated |
|----------- |------- |---------- |
|          A |      - |       0 B |
|          B |      1 |     496 B |
  • Выделенный содержит размер выделенной управляемой памяти. Stackalloc / native heap распределения не включены. Это за один вызов, включительно.
  • Gen X столбец содержит количество Gen X сборов на 1 000 операций. Если значение равно 1, то это означает, что GC собирает память один раз на тысячу вызовов тестов в генерации X, BenchmarkDotNet использует некоторую эвристику при выполнении тестов, поэтому количество вызовов может быть разным для разных прогонов. Масштабирование делает результаты сопоставимыми.
  • - в столбце Gen означает, что сборка мусора не производилась.
  • Если Gen X столбца нет, значит, сборка мусора для генерации не производилась X, Если ни один из ваших тестов не вызывает GC, столбцы Gen отсутствуют.

Читая результаты, имейте в виду, что:

  • 1 кБ = 1 024 байта
  • Каждый экземпляр ссылочного типа имеет два дополнительных поля: заголовок объекта и указатель таблицы методов. Вот почему результаты всегда включают 2x размер указателя для каждого выделения объекта. Для получения более подробной информации о дополнительных затратах, пожалуйста, прочитайте этот отличный пост в блоге. Как на самом деле работает Object.GetType()? Конрад Кокоса.
  • CLR выполняет выравнивание. Если вы попытаетесь выделить new byte[7] массив, он будет выделять byte[8] массив.

То, что показывает BenchmarkDotNet, называется "Трафик памяти" в dotMemory. Запустите ваше приложение под dotMemory с включенным " Начать сбор данных о распределении немедленно". Получите снимок памяти в конце сеанса профилирования, затем откройте представление " Трафик памяти". Вы увидите все объекты, выделенные и собранные во время сеанса профилирования.

Как насчет вашего вопроса об узких местах в памяти, так как все выделенные объекты собраны, потребление памяти не растет, и вы не видите никаких проблем в dotMemory.

Но 3 ГБ трафика в 6 секунд довольно велики, и это может повлиять на производительность, используйте dotTrace (в режиме временной шкалы), чтобы увидеть, какая часть этих 6 секунд расходуется в GC.

Хорошо, давайте пройдемся по одной итерации цикла:

  • Вы собираетесь выделить как минимум 1250 дюймов - так что давайте назовем это 5000 байтов или 5K.
  • Вы создадите словарь, содержащий те же самые целые числа и 1250 строк со средней длиной, скажем, 8 символов - поэтому давайте назовем это 20000 байтов или 20 КБ. Плюс накладные расходы Dictionary сам.
  • затем string.Join собирается использовать StringBuilder - так что это минимум дополнительных 20 КБ (вероятно, больше, поскольку массив динамически измеряется). затем ToString будет вызван на StrinBuilder (так еще 20К).

5К + 20К + 20К + 20К = 65К.

2,86 ГБ / 10000 = 0,286 МБ = около 286 КБ.

Итак, все это звучит примерно так. 65K - абсолютный минимум того, что может быть использование оперативной памяти. Фактор накладных расходов на конкатенацию строк при генерации значений словаря, накладные расходы на использование Dictionary (дополнительные массивы, дополнительные копии int и т. д.) и накладные расходы StringBuilder (который, вероятно, выделяет большие массивы несколько раз из-за длины строки), и вы можете легко получить от 65 -> 286.

Другие вопросы по тегам