Анализ производительности приложений включает не только поиск узких мест в вычислительной части на CPU, но и оптимизацию использования памяти, операций с диском и сетью, минимизацию времени выполнения и др.

В этой статье я расскажу о поиске проблем потребления памяти и о проекте Malloc_tracer, разработанном для отслеживания аллокаций.

Возможные сценарии анализа

Для улучшения профиля потребления памяти важно понимать, где и для чего выделяется память.

При воспроизводимом сценарии можно применить утилиту heaptrack, и увидеть стек вызовов для аллокаций. Но сценарии не всегда воспроизводятся.
Если на руках только боевой дамп от пользователя, то будут полезны инструменты вроде chap и Core Analyzer. Хотя они не дадут полной информации о месте создания аллокаций, но можно это сделать косвенно, определив тип данных в аллокациях.

Предположим, что случай воспроизводимый, но есть ограничение - приложение использует FANOTIFY для контроля файловых операций. В этом случае GDB и heaptrack не смогут работать: их подсоединение заблокирует приложение, ожидая разрешения на файловую операцию от этого приложения.

Тогда стоит изменить подход: модифицировать аллокатор так, чтобы он сохранял информацию о вызове malloc или new рядом с выделенной памятью. Это позволит, сняв дамп, понять, откуда взялась каждая конкретная аллокация. Так появился проект Malloc_tracer.

Описание Malloc_tracer

Основная идея Malloc_tracer проста: добавить к каждой аллокации 16 байт с адресом возврата и размером запрошенной памяти. Адрес возврата вызова malloc или new можно получить с помощью __builtin_return_address(0). Полный стек вызова здесь излишен, часто достаточно знать лишь одно место вызова.

Если положить размер и адрес после данных пользователя, то по дампу нельзя будет извлечь эту информацию. Связано это с тем, что стандартный malloc выделяет чаще всего больше памяти, чем попросят, и кладет именно этот размер в заголовок аллокации. Информация о размере, который нужен пользователю не сохраняется.
Для получения размера реально выделенной памяти можно использовать функцию malloc_usable_size(ptr). Тогда в дампе можно будет однозначно найти сохраненный адрес.

Плюсы решения:

  • Простота реализации.
  • Небольшой оверхед (дополнительные 16 байт практически не влияют на большие аллокации).
  • Возможность найти место создания каждой аллокации.
  • Не нужно подключаться к процессу.

Минусы:

  • На маленьких аллокациях размер служебной информации сопоставим с размером аллокации.
  • Требуется использование LD_PRELOAD.

Инструменты для анализа памяти

Malloc_tracer обеспечивает лёгкий способ записи информации о выделениях памяти при невозможности подключиться к процессу. Это полезное дополнение к уже существующим инструментам:

  • chap - утилита от разработчиков VMWare, позволяющая анализировать дампы без символов. Отличительная черта инструмента - для анализа не нужно изменять поведение программы для анализа. Из недостатоков отмечу, что в открытом доступе поддержан малый список аллокаторов. Я его использую для поиска аллокаций в дампе.
  • Core Analyzer - мощная штука, которая основана на ванильном GDB. Требует символьной информации, но поддерживающий широкий список аллокаторов.
  • heaptrack - профайлер памяти от KDE. Позволяет строить flamegraph, есть GUI. Нужно аттачиться к запущенному процессу.
  • gdb-heap - плагин для GDB для отслеживания аллокаций в дампах, написан на python. Использовал для вдохновения и создания своего плагина для GDB.
  • ebpf - швейцарский нож в мире Linux. С помощью него можно любые события, включая аллокации памяти.
  • jemalloc, jeprof - аллокатор с поддержкой профилирования. Использует статистическое сэмплирование, чтобы минимизировать оверхед, сохраняя при этом стек вызова. Можно посмотреть и на другие аллокаторы.
  • Valgrind (Massif + Memcheck) - мощный, но медленный инструмент для отслеживания аллокаций. heaptrack в целом предпочтительнее, если не нужно анализировать память на стеке - Massif это может.
  • ASan (AddressSanitizer) - не профайлер, а санитайзер от llvm. Помогает обнаруживать утечки памяти, ошибки use-after-free и обращения к освобожденной памяти. Отличается высокой скоростью.