Профилировщики памяти
128C# и .NET Framework --- Оптимизация приложений .NET Framework --- Профилировщики памяти
Исходники для статьиПрофилировщики этого типа выявляют операции выделения памяти в приложении и сообщают, какие методы выделяют больше всего памяти, какого типа объекты создаются и другие статистики, касающиеся памяти. Интенсивное выделение памяти в приложениях часто влечет за собой значительные накладные расходы па сборку мусора.
Выделение памяти в среде выполнения CLR является недорогой операцией, но ее освобождение может сопровождаться значительными накладными расходами. По этой причине группы небольших методов, выделяющих большие объемы памяти, могут отнимать совсем немного процессорного времени и быть практически незаметны в отчете профилировщика времени, но при этом вызывать значительные задержки на сборку мусора в случайных точках выполнения приложения. В своей практике мы встречали приложения, где память выделялась без должного внимания, и нам удавалось увеличивать их производительность - иногда в 10 раз - оптимизировав выделение памяти и управление ею.
Для профилирования выделения памяти мы будем использовать два профилировщика - вездесущий профилировщик Visual Studio, поддерживающий режим профилирования выделения памяти, и профилировщик CLR Profiler - самостоятельный и бесплатный инструмент. К сожалению, оба инструмента часто оказывают значительное влияние на производительность приложений, интенсивно использующих динамическую память, потому что для каждой операции выделения памяти профилировщик выполняет последовательность действий по сохранению информации для последующего составления отчетов. Тем не менее, результаты могут оказаться настолько ценными, что даже 100-кратное замедление при профилировании можно потерпеть.
Профилировщик выделения памяти Visual Studio
Профилировщик Visual Studio способен собирать информацию об операциях выделения памяти и жизненном цикле объектов (которые освобождаются сборщиком мусора) в обоих режимах, дискретном и инструментированном. В дискретном режиме профилировщик собирает информацию о выделении памяти в приложении в целом. В инструментированном режиме информация собирается только из инструментированных модулей.
Для экспериментов с профилировщиком Visual Studio вы можете использовать приложение JackCompiler.exe из исходников к этой статье. В мастере настройки профилировщика производительности выберите радиокнопку .NET memory allocation (Выделение памяти .NET). В конце сеанса профилирования, в представлении Summary (Сводка), будут показаны функции, выделившие памяти больше всего:
В представлении Functions для каждого метода будет указано количество объектов и количество байтов памяти, выделенных методом (как обычно, включительные и исключительные значения). В представлении Function Details (Сведения о функции) будет представлена информация о вызывающих и вызываемых функциях, а также указаны строки кода с объемами выделенной ими памяти в поле слева:
Но самая интересная информация содержится в представлении Allocation (Выделение), показывающем, какие ветви в стеке вызовов выделили памяти больше всего:
Позже мы узнаем, насколько важно отказаться от использования временных объектов, и обсудим феномен «кризиса среднего возраста» объектов, оказывающего существенное влияние на производительность, который проявляется в способности объектов переживать несколько циклов сборки мусора. Идентифицировать наличие этого явления в приложении можно с помощью представления Object Lifetime (Жизненный цикл объектов), сообщающем, в каком поколении объекты были утилизированы. Это представление поможет увидеть, имеются ли объекты, пережившие слишком много циклов сборки мусора. На рисунке ниже можно видеть, что все объекты строк, созданные приложением (и занимающие более 1 Гбайта памяти!) были утилизированы в нулевом поколении, а это означает, что ни одному из них не удалось прожить дольше одного цикла сборки мусора:
Хотя отчеты о выделении памяти, генерируемые профилировщиком Visual Studio, отличаются богатством информации, в них все же имеются некоторые недостатки. Например, трассировка стека вызовов с целью сгруппировать операции выделения памяти по типам объектов занимает достаточно много времени, если выделение памяти производится во множестве разных мест (что всегда верно для строк и массивов байтов). Профилировщик CLR Profiler поддерживает несколько дополнительных особенностей, делающих его ценной альтернативой профилировщику Visual Studio.
CLR Profiler
Профилировщик CLR Profiler - это отдельный инструмент профилирования, не требующий установки и занимающий менее 1 Мбайта дискового пространства. Как дополнительное преимущество, он распространяется с исходными текстами, которые наверняка заинтересуют тех, кто пожелает заняться созданием собственных инструментов, использующих CLR Profiling API. Он способен подключаться к выполняющимся процессам (если используется версия CLR не ниже 4.0) или запускать выполняемые файлы, и регистрировать все операции выделения памяти и события сборки мусора.
Пользоваться профилировщиком CLR Profiler очень просто - запустите профилировщик, щелкните на кнопке Start Application (Запустить приложение), выберите приложение для профилирования и дождитесь появления отчета - богатство информации для кого-то может оказаться ошеломляющим. Мы рассмотрим здесь некоторые отчеты профилировщика, а полное руководство вы найдете в документе CLRProfiler.doc, входящем в состав загружаемого пакета. Как обычно, для экспериментов с профилировщиком вы можете использовать пример приложения JackCompiler.exe или свое приложение.
На рисунке ниже показан главный отчет, появляющийся после завершения профилируемого приложения. Он содержит основные сведения, касающиеся выделения памяти и сборки мусора. Далее из этого отчета можно пойти в нескольких направлениях. Мы сконцентрируемся на исследовании источников выделения памяти, чтобы понять, в каком месте приложения создается больше всего объектов (этот отчет напоминает представление Allocation профилировщика Visual Studio). Мы могли бы заняться исследованием сборки мусора, чтобы узнать, какие объекты утилизируются. Наконец, можно было бы исследовать содержимое динамической памяти, чтобы получить представление о ее распределении.
Щелчок на кнопке Histogram (Гистограмма) рядом с полем Allocated bytes (Выделено байтов) или Final heap bytes (Конечный объем кучи в байтах) выведет гистограмму по типам объектов, сгруппированных по размерам. Эта гистограммы можно использовать для выявления больших и малых объектов, а также объектов, создаваемых чаще других. На рисунке ниже показана гистограмма для всех объектов, создаваемых нашим примером приложения:
Каждый столбик представляет объекты определенного размера. Легенда справа содержит общее число созданных экземпляров каждого типа и объем выделенной памяти в байтах.
Щелчок на кнопке Allocation Graph (График выделения) откроет отчет о выделении памяти в дереве стека вызовов для всех объектов в приложении. Информация в этом отчете сгруппирована так, чтобы легко можно было перейти от методов, выделивших больше всего памяти, к отдельным типам объектов и посмотреть, какие методы создали больше всего экземпляров этих типов. На рисунке ниже показана малая часть графика выделения памяти, начиная от метода Parser.ParseStatement(), выделившего (включительно) 372 Мбайт памяти и до различных методов, вызываемых им. (Кроме того, в остальных отчетах профилировщика CLR Profiler присутствует пункт контекстного меню Show who's allocated (Показать, для кого выделена память), открывающий отчет с графиком выделения памяти для подмножества объектов приложения.)
Здесь показаны только методы, информация о фактических типах объектов находится на графике правее.
Щелчок па кнопке Histogram by Age (Гистограмма по возрасту) откроет гистограмму, где объекты сгруппированы по возрасту. Она позволяет быстро обнаружить долгоживущие и временные объекты, что может пригодиться для борьбы с явлением «кризиса среднего возраста» объектов C#.
Щелчок на кнопке Objects by Address (Объекты по адресу) отобразит области управляемой динамической памяти в виде слоев; чем ниже слой, тем он старше. Подобно археологу вы можете погружаться в более глубокие слои и выяснять, какие объекты занимают память в вашем приложении. Этот отчет можно также использовать для диагностики фрагментации динамической памяти:
Метки слева - это адреса, метки «gen 0» и «gen 1» - это подразделы динамической памяти.
Наконец, щелчок на кнопке Time Line (График времени) в разделе Garbage Collection Statistics (Статистика сборки мусора) откроет отчет с информацией об отдельных циклах сборки мусора и их влиянии на динамическую память приложения:
Отметки на нижней оси представляют отдельные циклы сборки мусора, а в области графика изображается состояние управляемой динамической памяти. После сборки мусора объем используемой памяти резко уменьшается, а затем постепенно возрастает, до следующего цикла. В данном случае объем используемой памяти (после сборки мусора) остается постоянным, из чего следует, что в этом приложении отсутствуют утечки памяти.
Этот график можно использовать для определения типов утилизируемых объектов, и получения представления о том, как изменяется распределение динамической памяти после сборки мусора. С его помощью можно также выявлять утечки памяти, когда сборщик мусора освобождает недостаточно памяти, из-за того, что приложение продолжает удерживать все увеличивающееся количество объектов.
Графики и гистограммы выделения памяти - весьма полезные инструменты анализа, но иногда важнее бывает выявить ссылки между объектами, а не объемы выделяемой памяти в разных методах. Например, когда в приложении обнаруживаются утечки управляемой памяти, очень полезно пройтись по динамической памяти, чтобы найти категории объектов, занимающие наибольшие объемы памяти и узнать, какие объекты на них ссылаются, мешая сборщику мусора утилизировать их. Пока профилируемое приложение выполняется, щелкните на кнопке Show Heap now (Показать кучу сейчас), чтобы сгенерировать дамп динамической памяти для последующего исследования с целью классификации ссылок между объектами.
На рисунке ниже показан отчет профилировщика сразу с тремя дампами динамической памяти, расположенными друг над другом, показывающий увеличение количества объектов типа byte[], удерживаемых очередью объектов, готовых к завершению (freachable queue), из-за наличия ссылок на них в объектах Employee и Schedule:
Ha рисунке ниже показаны результаты выбора пункта Show New Objects (Показать новые объекты) контекстного меню, чтобы оставить в отчете только объекты, созданные в период времени между сохранением второго и третьего дампов:
На этом графике видно, что источником утечки памяти является цепочка ссылок из очереди объектов, готовых к завершению.
Дампы динамической памяти, созданные профилировщиком CLR Profiler, можно использовать для диагностики утечек памяти в приложениях, но средств визуализации, упрощающих это, в данном профилировщике явно недостаточно. Коммерческие инструменты, которые мы рассмотрим далее, предлагают более богатые возможности, включая автоматические средства обнаружения наиболее типичных источников утечек памяти, разнообразные фильтры и возможности более сложной группировки информации. Поскольку большинство из этих инструментов не сохраняют информацию о каждом объекте, размещенном в динамической памяти, и не сохраняют информацию о выделении памяти в разных методах, они имеют более низкие накладные расходы, что само по себе является большим преимуществом.
Коммерческие профилировщики памяти
В этом разделе мы познакомимся с двумя коммерческими профилировщиками памяти, специализирующимися на визуализации состояния динамической памяти и выявлении источников утечек памяти. Поскольку эти инструменты отличаются большой сложностью, мы исследуем лишь малое подмножество их особенностей и оставим вам возможность ознакомиться с остальными самостоятельно, прочитав соответствующие руководства.
Профилировщик памяти ANTS
Профилировщик ANTS Memory Profiler компании RedGate специализируется на анализе срезов динамической памяти. Ниже подробно описывается процесс использования ANTS Memory Profiler для диагностики утечек памяти. Если у вас есть желание самим повторить описываемые действия, загрузите пробную 14-дневную версию ANTS Memory Profiler, которую можно найти по адресу: ANTS Memory Profiler, и используйте ее для профилирования собственного приложения. Описание и скриншоты, представленные ниже, относятся к версии ANTS Memory Profiler 7.3, которая была последней на момент написания данных строк.
Следуя за экспериментом, описываемым ниже, вы можете использовать пример приложения FileExplorer.exe из папки с исходниками. Это приложение имитирует утечку памяти, выполняя обход дерева каталогов и не освобождая объекты с информацией о непустых каталогах.
Запустите приложение из профилировщика. (Подобно профилировщику CLR Profiler, ANTS поддерживает возможность подключения к выполняющимся процессам, начиная с версии CLR 4.0.)
По завершении инициализации приложения щелкните на кнопке Take Memory Snapshot (Сделать снимок памяти). Этот снимок будет служить основой для последующих исследований.
Накопив утечки памяти, сделайте еще один снимок динамической памяти.
После завершения приложения сравните снимки (базовый снимок с последним или промежуточные друг с другом) чтобы определить, какие типы объектов приводят к увеличению объема занимаемой памяти.
Выберите определенный тип и щелкните на кнопке Instance Categorizer (Классификатор экземпляров), чтобы понять, какие ссылки удерживают в памяти объекты подозреваемого в утечках типа. (На этом этапе исследуются ссылки между типами - экземпляры типа A, ссылающиеся на экземпляры типа B, будут сгруппированы по типу.)
Исследуйте отдельные экземпляры подозреваемых в утечках типов, щелкнув на кнопке Instance List (Список экземпляров). Выберите несколько наиболее представительных экземпляров и щелкните на кнопке Instance Retention Graph (График зависимостей экземпляров), чтобы посмотреть, почему они удерживаются в памяти. (На этом этапе исследуются ссылки между отдельными объектами, и здесь можно выяснить причину, мешающую сборщику мусора утилизировать конкретные объекты.)
Вернитесь в исходный код приложения и измените его так, чтобы объекты, вызывающие утечку, своевременно уничтожали ссылки на проблематичные цепочки.
К концу процесса анализа у вас должно сложиться четкое представление, почему самые тяжеловесные объекты в вашем приложении не утилизируются сборщиком мусора. Существует множество причин, вызывающих утечки памяти, и их выявление проблемных объектов из миллионов имеющихся - это целое искусство.
На рисунке ниже показан пример сравнения двух снимков динамической памяти. Основные утечки памяти (в байтах) связаны с объектами string.
Детальный осмотр типа string после щелчка на кнопке Instance Categorizer (Классификатор экземпляров) наводит на мысль, что некоторое событие создает экземпляры FileInformation в памяти, которые в свою очередь хранят ссылки па объекты byte[]:
Как видно из рисунка, строки удерживаются в памяти массивами строк, которые сами удерживаются экземплярами типа FileInformation, которые в свою очередь удерживаются событием (через делегаты System.EventHandler).
Более детальное исследование конкретных экземпляров в представлении, выводимом после щелчка на кнопке Instance Retention Graph (График зависимостей экземпляров) в результате указывает, что источником утечек является статическое событие FileInformation.FileInformationNeedsRefresh:
Профилировщик памяти SciTech .NET
Профилировщик SciTech .NET Memory Profiler - еще один коммерческий инструмент, предназначенный для выявления утечек памяти. В общем и целом процесс анализа в этом профилировщике похож на процесс анализа с использованием ANTS Memory Profiler. Однако в отличие от последнего он способен открывать аварийные файлы дампов памяти, что дает возможность применять его не только для профилирования приложения, но и для анализа аварийных дампов памяти, созданных средой выполнения CLR при исчерпании доступной памяти. Эта возможность может пригодиться для диагностики проблем уже после аварии. Загрузить 10-дневную пробную версию можно по адресу: http://memprofiler.com/download.aspx. Описание и скриншоты, представленные ниже, относятся к версии .NET Memory Profiler 4.0, которая была последней на момент написания данных строк.
Чтобы открыть файл с аварийным дампом памяти в профилировщике .NET Memory Profiler, выберите пункт меню File --> Import memory dump (Файл --> Импортировать дамп памяти) и укажите профилировщику нужный файл. Если имеется несколько файлов дампов, их все можно импортировать в один сеанс анализа и сравнить как снимки динамической памяти. Процесс импорта может занимать довольно продолжительные промежутки времени, особенно для больших дампов памяти. Чтобы ускорить работу с сеансами анализа, в состав SciTech входит отдельный инструмент, NmpCore.exe, который можно использовать для сохранения сеанса в промышленном окружении.
На рисунке ниже показаны результаты сравнения двух дампов памяти в профилировщике .NET Memory Profiler. Он немедленно обнаруживает проблемные объекты и сразу же сообщает, что они удерживаются в памяти обработчиками событий и предлагает выполнить анализ объектов FileInformation.
В пятом столбце списка указано число хранящихся в памяти экземпляров, а в седьмом - количество байтов, занимаемых ими. Основной объем памяти занимают объекты string, информация о которых скрыта здесь за всплывающей подсказкой.
Подробный отчет об объектах FileInformation показывает, что все выбранные экземпляры FileInformation связаны с обработчиком событий FileInformation.FileInformationNeedsRefresh, отчет с информацией об отдельных экземплярах показывает ту же цепочку ссылок, которую мы видели, когда знакомились с профилировщиком ANTS Memory Profiler:
Мы не будем здесь повторять инструкции по использованию, которые уже давались в описании профилировщика ANTS .NET Memory Profiler - отличное руководство по профилировщику SciTech можно найти на сайте проекта: memprofiler.com/OnlineDocs/. Этим инструментом завершается наш обзор инструментов и приемов выявления утечек памяти, начатый со знакомства с CLR Profiler.