Профайлер памяти — это инструмент, который анализирует работу конкретного приложения, помогает выявить утечки памяти, показывает точное количество использованной памяти и количество выделений. Если программа потребляет больше памяти, чем ожидалось, то профайлер покажет все выделения ресурсов и позволит определить, почему и где они выделяются.
Профайлер можно использовать не только в разработке, но и в системном администрировании, чтобы отслеживать рабочие процессы конкретных приложений, скриптов и задач.
Профилирование — сбор характеристик работы программы, таких как время выполнения отдельных фрагментов (подпрограмм), число верно предсказанных условных переходов, число кэш-промахов и т. д. Соответственно, инструмент для профилирования памяти называется профилировщиком или профайлером памяти. Часто его использование сочетается с оптимизацией программы.
Такие инструменты нужны для оценки того, как хорошо спроектирована программа, а также чтобы проанализировать её и идентифицировать критические участки.
Часто профилирование используется, чтобы определить время выполнения определённых частей программы, частоты их выполнения или сгенерировать граф вызовов (call graph). Обычно это нужно для выявления самых часто выполняемых частей программы с целью оптимизации.
Профайлер выдаёт информацию в разных форматах:
- статистическая сводка наблюдаемых событий (профиль) с аннотацией к операциям исходного кода, в которых происходят события,
- поток записанных событий (трассировка), что больше актуально для параллельных программ, где ожидание сообщений или проблемы синхронизации часто зависят от временнóй зависимости событий,
- взаимодействие с гипервизором (непрерывный или периодический мониторинг, например, с помощью экранного дисплея). Это даёт возможность включать или выключать трассировку в любой нужный момент выполнения, а также просматривать текущие метрики о (всё ещё выполняющейся) программе. Также даёт возможность приостанавливать асинхронные процессы в критических точках, чтобы более детально изучить взаимодействие с другими параллельными процессами.
Базовый инструмент профилирования в первых версиях Unix назывался
prof
, он показывает вызов каждой функции программы и сколько времени это занимает. В 1982 году вышла программа gprof
, которая расширила эту концепцию до полного анализа графа вызовов.В 1994 году была опубликована работа с описанием ATOM (Analysis Tools with OM). Платформа ATOM превращает программу в свой собственный профилировщик: во время компиляции она вставляет код в анализируемую программу. Этот вставленный код выводит данные анализа:
Процесс ATOM
Раскладка памяти с данными анализа
В 2004 году статьи о gprof и ATOM попали в список 50-ти самых влиятельных статей конференции по дизайну и реализации языков программирования ACM SIGPLAN PLDI за 20 лет (1979−1999 гг).
Сейчас существуют различные инструменты в этой области. Среди широко используемых статистических профайлеров для Java/управляемого кода можно назвать AQtime от SmartBear Software и CLR Profiler от Microsoft. Эти профайлеры также поддерживают профилирование нативного кода, наряду с Shark от Apple (OSX), OProfile (Linux), Intel VTune и Parallel Amplifier (часть Intel Parallel Studio), Oracle Performance Analyzer. Для кода на Python есть профайлер памяти Memray, для программ Android свой инструмент и т. д.
Из последних новинок можно назвать Bytehound — относительно новый профайлер памяти под Linux, который вышел несколько лет назад, последняя версия 0.11.0 от ноября 2022 года.
▍ Bytehound
Функции Bytehound:
- Собственный графический веб-интерфейс, который можно использовать для анализа.
- Регистрирует каждое выделение и удаление памяти, а также полные трассировки стека:
- Может динамически отсеивать временные выделения, позволяя составлять профиль за длительный период времени. На скриншоте показано сводная статистика по объёму выделенной памяти для программы:
- Использует собственную реализацию развёртывания стека, что делает его намного эффективнее других подобных инструментов, в некоторых случаях на порядки быстрее.
- Может экспортировать собранные данные в различные форматы: в JSON (для самостоятельного анализа), в Heaptrack (там отличный GUI) и в flamegraph.
Графический интерфейс Heaptrack
- Может динамически передавать данные профилирования на другую машину вместо сохранения их локально, что полезно для профилирования на системах с ограниченным объёмом памяти.
- Поддерживает AMD64, ARM, AArch64 и MIPS64 (где для MIPS64 требуется крошечный патч ядра вне дерева для
perf_event_open
)
- Поддерживает профилирование приложений, использующих
jemalloc
в качестве аллокатора (работает только на AMD64 с крейтомjemallocator
)
- Поддерживает встроенный DSL на основе Rhai для программного и/или автоматического анализа данных.
Вот как выглядит написание скриптов в консоли с отображением в GUI:
Для сборки программы из исходников требуется GCC, cargo, Rust Nightly (версия 1.62 или выше) и пакетный менеджер Yarn (для сборки GUI).
После установки всех требуемых зависимостей можно собрать Bytehound:
cargo new bytehound-preload --bin
cargo new bytehound-cli --bin
cargo build --release -p bytehound-preload
cargo build --release -p bytehound-cli
После этого бинарники Bytehound можно найти в директории
target/release/
:Как вариант, предварительно сконфигурированные бинарники выкладываются в репозитории.
Данные профилирования собираются, если прикрепить профайлер к своему приложению с помощью команды
LD_PRELOAD
. Нужно просто поместить libbytehound.so
в каталог со своей программой, а затем выполнить следующее:$ export MEMORY_PROFILER_LOG=info
$ LD_PRELOAD=./libbytehound.so ./ваша_программа
Впоследствии можно дополнительно настроить профайлер с помощью переменных окружения, хотя обычно в этом нет необходимости.
Собранные данные загружаются для анализа:
$ ./bytehound server memory-profiling_*.dat
Веб-интерфейс профайлера доступен в браузере по адресу
http://localhost:8080
, там отображаются результаты анализа. Документация доступна здесь. В ней написано, что профайлер может упасть при попытке загрузить данные, обычно это свидетельствует о недостатке оперативной памяти для работы. Из типичных проблем называют ещё слишком большой объём собранных данных, так что они не помещаются на диске. В таком случае рекомендуется перед началом профилирования установить значение переменной
MEMORY_PROFILER_CULL_TEMPORARY_ALLOCATIONS
на 1
, чтобы не сохранять большое количество недолгоживущих (временных) выделений, это уменьшит размер результирующего файла. Кроме того, можно настроить параметр MEMORY_PROFILER_TEMPORARY_ALLOCATION_LIFETIME_THRESHOLD
, чтобы указать, какие выделения профайлер будет считать временными.Если файл всё равно оказался слишком большим и не загружается из-за нехватки RAM, можно воспользоваться подкомандой
strip
для удаления части записей о выделениях. Например, такая команда удаляет все выделения продолжительностью менее 60 секунд:$ ./bytehound strip --threshold 60 -o stripped.dat original.dat
▍ Пример анализа
В каталоге
/simulation
официального репозитория лежат тестовые данные с анализируемой программой. Там демонстрируются скрипты, с помощью которых анализируется эта программа (доступны через подкоманду script
в консоли или через отдельную консоль скриптов в GUI, как на скриншоте вверху). Пример в документации демонстрирует правильный образ мышления для разработчика, который анализирует данные в поисках утечек памяти.
Первый шаг: посмотреть все выделения.
graph()
.add(allocations())
.save();
Налицо признаки утечки памяти, но из общей диаграммы сложно понять что-то конкретное.
Второй шаг: найти все очевидные утечки памяти, то есть выделения памяти, которая не освобождалась обратно.
graph()
.add("Leaked", allocations().only_leaked())
.add("Temporary", allocations())
.save();
Здесь уже виден линейный график (утечка), который можно разделить на части по трассировке:
let groups = allocations()
.only_leaked()
.group_by_backtrace()
.sort_by_size();
graph().add(groups).save();
Эти выделения можно рассмотреть подробнее. Например, самая большая группа:
analyze_group(groups[0]);
Выдача:
Total: 1646 Leaked: 1646 Backtrace: #00 [simulation] _start [start.S:115] #01 [libc.so.6] __libc_start_main #02 [libc.so.6] 7f8bb4f5128f #03 [simulation] main #17 [simulation] simulation::main [main.rs:122] #18 [simulation] simulation::allocate_linear_leak_never_deallocated [main.rs:32] #19 [simulation] alloc::vec::Vec<T,A>::resize [mod.rs:2366]
Здесь видно, что в трассировке сплошные утечки (1646 утечек). Просмотр других групп тоже показывает интересные результаты.
Третий шаг: более глубокий анализ.
Кроме явных утечек памяти (выделения памяти, которые никогда не освобождаются), есть ещё выделения памяти, которые освобождаются только при закрытии программы. Для этого применяется один из фильтров:
let leaked_until_end = remaining
.only_leaked_or_deallocated_after(data().runtime() * 0.98);
graph().add(leaked_until_end).save();
Если очистить и выполнить трассировку этих временных выделений и утечек, то можно в итоге всё-таки выйти на главный источник утечки:
В итоге можно найти конкретные функции в программе, которые генерируют эти утечки.
Некоторые приведённые результаты можно посмотреть в GUI даже без консольных команд, просто на вкладках интерфейса.
Подводя итог, можно сказать, что профайлеры памяти анализируют утечки, определяют места, где конкретно расходуется память, показывают выделения памяти и сообщают о чрезмерной фрагментации, если такая есть. Это полезные инструменты в разработке ПО и администрировании сервера.
Telegram-канал со скидками, розыгрышами призов и новостями IT ?
Комментарии (2)
sv91
10.07.2024 13:19+1Насколько сильно замедляет выполнение профайлинга программу? По опыту знаю, что инструмент от gmalloc-а замедляет очень сильно, до степени, что результат профайлинга может оказаться бесполезным
okhsunrog
А причем тут
cargo new
? Это создание нового проекта, и для сборки эти команды совсем не нужны. Да и Rust Nightly не нужен ему давно, всё уже в Stable завезли. Прекрасно собралось со стабильным 1.79.0 тулчейном. Вообще, проект выглядит заброшенным, последний коммит год назад, на issues уже почти год никто не отвечает. А так, выглядит интересно, да