Измеряйте перед оптимизацией
Каждая оптимизация производительности должна начинаться с измерения. Оптимизация по интуиции производит код, выглядящий быстрее, но работающий медленнее, или быстрый код в путях вне горячего пути.
Профилировщик говорит:
- Где реально тратится время — часто не там, где думаете
- Почему медленно — промахи кэша, неверные предсказания переходов, конкуренция за блокировку
- Помогло ли ваше исправление — сравнение до/после
Два инструмента покрывают большинство задач производительности C++ на Linux: perf для
счётчиков аппаратной производительности и анализа на уровне CPU; valgrind и его
инструменты для ошибок памяти, симуляции кэша и профилирования графа вызовов.
perf — счётчики аппаратной производительности Linux
perf читает счётчики PMU (Performance Monitoring Unit) CPU: выполненные инструкции,
промахи кэша, неверные предсказания переходов. Низкие накладные расходы (< 1%) и не
требует инструментирования бинарного файла.
Сборка для профилирования
Всегда профилируйте с сохранёнными отладочными символами:
1# CMake Release-сборка с символами
2cmake -DCMAKE_BUILD_TYPE=RelWithDebInfo ..
3make -j4
4
5# Или вручную
6g++ -O2 -g -fno-omit-frame-pointer -o myapp src/main.cpp
-fno-omit-frame-pointer сохраняет цепочку указателей фрейма — требуется для точного
разворачивания стека вызовов в perf record.
perf stat — быстрый обзор
1perf stat ./myapp
Вывод:
Performance counter stats for './myapp':
3,421.45 msec task-clock # 0.998 CPUs utilized
12 context-switches # 3.507 /sec
3 cpu-migrations # 0.877 /sec
231 page-faults # 67.514 /sec
9,847,234,821 cycles # 2.877 GHz
7,203,441,208 instructions # 0.73 insn per cycle
542,112,034 cache-misses # 14.22% of all cache refs
38,023,112 branch-misses # 2.31% of all branches
Ключевые числа:
- insn per cycle (IPC): 0.73 — плохо, 1.0+ — норма для последовательного кода. Низкий IPC означает, что CPU простаивает, обычно ожидая памяти.
- cache-misses 14%: всё выше ~2% на горячей нагрузке — существенно. Этот бинарный ограничен памятью.
- branch-misses 2.31%: умеренно — исследуйте, если это в горячем пути.
perf record + report — найти горячие функции
1perf record -g ./myapp # -g = граф вызовов (трассировки стека)
2perf report
perf report открывает интерактивный TUI. Навигация стрелками, Enter для раскрытия
вызывающих/вызываемых функции.
Overhead Command Shared Object Symbol
38.21% myapp myapp [.] processBuffer
22.14% myapp myapp [.] findNearest
15.43% myapp libc.so.6 [.] memcpy
8.22% myapp myapp [.] SensorParser::parse
processBuffer занимает 38% CPU. Раскройте, чтобы увидеть, какие вызывающие её достигают
и где она тратит время.
perf annotate — просмотр на уровне исходного кода
1perf annotate processBuffer
Показывает строки источника с процентом семплов:
│ for (size_t i = 0; i < count; ++i) {
38.2 │ float v = data[i]; ← 38% времени здесь
1.2 │ v = filter(v);
52.1 │ output[i] = v * scale; ← 52% здесь
Если 90% времени приходится на загрузки из памяти (data[i], output[i]), узкое место —
пропускная способность памяти или промахи кэша, а не вычисления.
perf stat для конкретных событий
1# Анализ кэша
2perf stat -e cache-references,cache-misses,L1-dcache-misses ./myapp
3
4# Анализ переходов
5perf stat -e branches,branch-misses ./myapp
6
7# Пропускная способность памяти (Intel)
8perf stat -e mem_inst_retired.all_loads,mem_inst_retired.all_stores ./myapp
valgrind — ошибки памяти и симулированный кэш
valgrind запускает бинарный файл в инструментированной виртуальной машине.
Накладные расходы значительны (10–50×), но ловит ошибки, которые аппаратные
профилировщики пропускают.
memcheck — ошибки памяти
1valgrind --leak-check=full --track-origins=yes ./myapp
==12345== Invalid read of size 4
==12345== at 0x401234: processBuffer (main.cpp:47)
==12345== by 0x401500: main (main.cpp:112)
==12345== Address 0x5204080 is 0 bytes after a block of size 64 alloc'd
==12345== at 0x4C2FB0F: malloc (in /usr/lib/valgrind/vgpreload_memcheck.so)
==12345== by 0x401100: Buffer::Buffer(int) (buffer.cpp:12)
Это чтение за пределами буфера — чтение 4 байт ровно в конце выделения 64 байт. Показаны номера строк; следуйте стеку для поиска ошибки.
Типичные находки memcheck:
- Invalid read/write: переполнение буфера, ошибка на единицу
- Use of uninitialised value: чтение до записи (условные переходы на неинициализированных данных)
- Definitely lost / still reachable: утечки памяти
cachegrind — симуляция кэша
1valgrind --tool=cachegrind ./myapp
2cg_annotate cachegrind.out.<pid>
Симулирует L1/L2/L3 кэш (настраиваемые размеры) и сообщает о попаданиях/промахах на
строку источника. Детальнее, чем события кэша perf stat; работает без root.
--------------------------------------------------------------------------------
Ir I1mr ILmr Dr D1mr DLmr Dw D1mw DLmw
--------------------------------------------------------------------------------
700,000 0 0 350,000 25,000 25,000 350,000 0 0
D1mr = промахи L1 при чтении данных. 25,000 промахов на 350,000 чтений = 7%
промахов в этой функции. Если каждый промах обращается к DRAM — это ~10 мкс простоев.
callgrind — граф вызовов + количество инструкций
1valgrind --tool=callgrind ./myapp
2callgrind_annotate callgrind.out.<pid>
3# Или визуализировать с KCachegrind
4kcachegrind callgrind.out.<pid>
KCachegrind показывает граф вызовов с количеством инструкций и промахами кэша на ребро вызова. Полезен для определения, какой вызывающий отвечает за горячий путь.
AddressSanitizer — быстрее valgrind для ошибок памяти
Для отладочных сборок AddressSanitizer находит те же классы ошибок, что memcheck, но с 2× накладными расходами вместо 50×:
1# Компиляция с ASan
2g++ -fsanitize=address -g -O1 -fno-omit-frame-pointer -o myapp src/main.cpp
3./myapp
==12345==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x602000000090
READ of size 4 at 0x602000000090 thread T0
#0 0x401234 in processBuffer main.cpp:47
#1 0x401500 in main main.cpp:112
Используйте ASan в CI при каждой сборке. Используйте valgrind memcheck для производственных бинарных файлов (перекомпиляция не нужна) или когда ASan недоступен.
Рабочий процесс
1. Сборка с -O2 -g -fno-omit-frame-pointer
2. perf stat — ограничен CPU или памятью?
IPC < 0.7 или cache-misses > 5% → ограничен памятью → смотреть организацию данных
IPC > 1.5, мало промахов кэша → ограничен вычислениями → смотреть горячие функции
3. perf record -g + perf report — какая функция?
Найти 1-3 функции с наибольшими накладными
4. perf annotate — какая строка?
Найти конкретный кластер инструкций
5. Определить первопричину:
Загрузка из памяти на каждой итерации → промах кэша → реструктурировать данные (AoS→SoA)
Ветвление внутри цикла → неверное предсказание → убрать ветвление или отсортировать данные
Блокировка в горячем пути → конкуренция → lock-free структура или уменьшить область видимости
6. Исправить, пересобрать, снова perf stat — сравнить IPC и процент промахов кэша
Типичные находки и исправления
| Находка perf | Вероятная причина | Исправление |
|---|---|---|
| IPC < 0.5, cache-misses > 10% | Случайный паттерн доступа к памяти | SoA, prefetch, пул-аллокатор |
| IPC < 0.5, cache-misses низкие | Инструкция с большой задержкой | Уменьшить деление, sqrt, невыровненные загрузки |
| Branch-misses > 5% в горячем цикле | Непредсказуемый переход | Сортировка входных данных, безветвительная арифметика |
memcpy/memmove в топе |
Ненужное копирование | std::move, span, reserve() |
| Блокировка/мьютекс в топ-10% | Конкуренция потоков | Lock-free очередь, уменьшить критическую секцию |
malloc/new в горячем пути |
Выделение на каждой итерации | Пул-аллокатор, арена, предварительное выделение |
Итоги
- Собирайте с
-O2 -g -fno-omit-frame-pointerдля профилирования perf statсначала: IPC и процент промахов кэша говорят, ограничены ли вы вычислениями или памятьюperf record -g+perf report: какая функция, какой вызывающийperf annotate: какая строка источникаvalgrind --tool=cachegrind: симуляция промахов кэша с аннотацией источникаvalgrind memcheck/ AddressSanitizer: ошибки памяти — запускать в CI, не в цикле профилирования- Исправляйте, измеряйте снова — никогда не угадывайте, всегда проверяйте
Что дальше
- Кэш-дружественные структуры данных — исправление паттернов, ограниченных памятью, которые выявляет perf
- Распределители памяти — устранение
mallocиз горячих путей - Lock-free очереди — устранение конкуренции за мьютекс