Производительность C++ — измеряй, потом оптимизируй
Главная ошибка в оптимизации — оптимизировать без замера. Дональд Кнут: «Преждевременная оптимизация — корень всех зол». Это не «не оптимизируй», а «сначала измерь, где медленно». В 95% случаев узкое место — не там, где вы думали.
Карта темы
- Профилирование — sampling vs instrumentation, инструменты (
perf, VTune, callgrind, gprof). - Бенчмаркинг — микробенчмарки на Google Benchmark, что они измеряют и что нет.
- Техники оптимизации — алгоритмическая, cache-friendly, branch prediction, SIMD, inlining.
- Антипаттерны — преждевременная оптимизация, оптимизация без измерения, оптимизация холодного кода.
Профилирование
Прежде чем что-то ускорять, найди где медленно. Два класса инструментов:
Sampling profilers — периодически (1000–10000 Hz) снимают call stack. Низкий overhead (1–5%), пригодны для production.
perf record/report(Linux) — стандарт. Использует PMU CPU.- VTune (Intel) — GUI, hotspots, micro-architecture analysis, memory access patterns.
- Instruments (macOS) — встроен в Xcode, time profiler + allocations.
Instrumentation profilers — вставляют замеры на вход/выход каждой функции. Высокий overhead (2-10x), точные счётчики вызовов.
- callgrind (Valgrind) — медленный, но даёт точный call graph и cache simulation.
- gprof — устарел, оставлен для legacy.
⚠️ Профилируй release-сборку с -fno-omit-frame-pointer -g. Debug-сборка покажет ложные узкие места.
Бенчмаркинг
Google Benchmark — стандарт для микробенчмарков. Корректно усредняет повторы, борется с dead-code elimination через benchmark::DoNotOptimize.
static void BM_VectorPush(benchmark::State& state) {
for (auto _ : state) {
std::vector<int> v;
for (int i = 0; i < state.range(0); ++i) v.push_back(i);
benchmark::DoNotOptimize(v);
}
}
BENCHMARK(BM_VectorPush)->Range(8, 8<<10);
⚠️ Микробенчмарк показывает скорость изолированной операции. Он не отвечает «насколько ускорится моя программа» — для этого нужен профиль реальной нагрузки.
Техники оптимизации (в порядке окупаемости)
- Алгоритмическая. O(n²) → O(n log n) — обычно 10–1000× ускорение. Любой другой пункт даёт максимум 2–10×.
- Cache-friendly данные.
std::vectorвместоstd::list. SoA (Structure of Arrays) вместо AoS, когда читаешь по полю. - Reduce allocations. Heap-аллокация — 100–1000 циклов. Reserve, reuse, pool, stack-buffer.
- Branch prediction. Сортировка ввода может сделать ветвление предсказуемым и ускорить цикл в 2–3 раза.
- Inlining. Маленькие функции в hot-loop. Компилятор делает большую часть сам;
[[gnu::always_inline]]для крайних случаев. - SIMD. AVX/AVX2/AVX-512 — ускорение для числового кода в 4–16×. Autovectorization компилятора +
-O3 -march=nativeпокрывает 80% случаев. - Параллелизм. Threads,
std::execution::par, GPU. Окупается, когда последовательная версия уже оптимизирована — иначе ускоряете медленный код параллельно.
Антипаттерны
- «Заменим
std::stringнаchar*, будет быстрее». Без профиля не быстрее, читаемее не станет, баги вstrlenначнутся. inlineключевое слово ради скорости. Современныйinlineпро ODR, не про оптимизацию. Компилятор решает сам.- Замена
i++на++iв цикле для int. Компилятор давно генерирует одно и то же. Имеет смысл для итераторов с тяжёлым деструктором временного. - Микрооптимизация холодного кода. Если функция занимает 0.1% профиля, ускорение её вдвое улучшит программу на 0.05%.
- Оптимизация в debug-сборке. Замеры в debug бессмысленны — нет inlining, нет vectorization, есть iterator debugging.
Частые ошибки и ловушки
| Ошибка | Последствие |
|---|---|
| Оптимизация без профиля | Ускоряете то, что не было узким местом |
| Бенчмарк в debug-сборке | Цифры в 5–20 раз отличаются от release |
| Микробенчмарк → выводы о реальной программе | Cache state и contention в продакшене иные |
benchmark::DoNotOptimize забыт | Компилятор выкидывает «пустое» вычисление, бенчмарк измеряет ноль |
| Параллелизация неоптимизированного кода | Ускоряете медленный baseline в N раз — всё равно медленно |
SIMD руками вместо -O3 -march=native | Тратите неделю; компилятор давно генерирует SIMD сам |
| Игнорирование cache miss | Алгоритмически O(n) код тормозит из-за random access |
Значение для собеседований
Performance — частая senior-тема, особенно в trading, gamedev, embedded, баз данных. Интервьюер проверяет два навыка:
- Метод: «Программа медленная — твои действия?» Правильный ответ начинается с «профилирую и нахожу горячую функцию», а не с «перепишу на std::array».
- Знание инструментов: знаешь ли ты разницу между sampling и instrumentation, что измеряет cachegrind, когда применять
perfи когда VTune.
Типичный неправильный ответ: «Я бы заменил std::string на char* и std::vector на raw-массив». Это карго-культ. Без профиля — оптимизация наугад, и обычно вред больше пользы.
Популярные направления:
- Программа медленная. Как ты будешь её профилировать?
- Чем sampling profiler отличается от instrumentation?
- Что такое cache miss и как его обнаружить?
- Когда
inlineвлияет на производительность, а когда нет? - SIMD: что это и в каких случаях писать руками, а не полагаться на компилятор?